mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-05 20:15:15 +00:00
Add entityReferences and entityReferenceList as custom properties (#15448)
* Add entityReferences and entityReferenceList as custom properties * ui: allow to add custom properties for all available property types. * make entity type as an array * Fix label typo and import order in AddCustomProperty component * Remove unnecessary property types with format * Refactor CustomPropertyTable.tsx to handle array config and enum config * Add entity reference options and update custom property form * Add EntityReference and EntityReferenceList as a custom property * add validation for date format * Add date and dateTime input support to PropertyValue component * Fix import order and add search functionality to type dropdown * Fix custom property cypress tests * add input for number, email, timestamp, timeInterval, duration, time, sqlQuery, * add input support for entityReference and entityReferenceList * Add placeholders for email, timestamp, start time, end time, and duration inputs * Refactor PropertyValue component to use destructuring for timeInterval object * Add minWidth style to PropertyValue component and include SQL query editor * Add entityReference and entityReferenceList as a generic types and not reference to the existing types * Remove services from entity reference types list * handle property values for different property type * Update ExtensionTable to handle object values in CustomPropertyTable * Add entity reference list rendering and styling * Fix file paths in complexTypes.json * Add regex constant for UNIX timestamp in milliseconds and update language files * Refactor custom property configuration and add new options * Fix import order and update property value handling * add unit tests for different input types for different property type * add unit test for different property type values * fix cypress tests * add cypress test for CRUD for different types * add cypress test for entity ref and entity ref list property type * add cypress tests for all the 11 entities which support custom properties * fix cypress test for createCustomPropertyForEntity * Add new interfaces and types for CustomPropertyTable * add cypress test for custom property to create via APIs for entities * add cypress tests for string,integer, markdown, number, duration, email * add cypress test for enum property for all entities * add cypress test for sqlQuery, timestamp and timeInterval property * fix cypress test for sqlQuery input * fix flaky cypress test for sqlQuery input * address comments * Refactor import statements and update ENTITY_PATH enum * Update import statement in CustomProperty.ts * change TimeInterval to Time Interval --------- Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
parent
891e0bf893
commit
7a3a271f26
@ -386,6 +386,18 @@
|
||||
<version>3.25.3</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.skyscreamer</groupId>
|
||||
<artifactId>jsonassert</artifactId>
|
||||
<version>1.5.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.vaadin.external.google</groupId>
|
||||
<artifactId>android-json</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- JSON-P: Java API for JSON Processing (JSR 374) -->
|
||||
<dependency>
|
||||
<groupId>javax.json</groupId>
|
||||
|
||||
@ -53,6 +53,7 @@ import org.openmetadata.service.util.EntityUtil;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
import org.openmetadata.service.util.TestUtils;
|
||||
import org.openmetadata.service.util.TestUtils.UpdateType;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
@ -364,6 +365,14 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
assertEquals(expected.getSchema(), patched.getSchema());
|
||||
assertEquals(expected.getCategory(), patched.getCategory());
|
||||
assertEquals(expected.getNameSpace(), patched.getNameSpace());
|
||||
try {
|
||||
JSONAssert.assertEquals(
|
||||
JsonUtils.pojoToJson(expected.getCustomProperties()),
|
||||
JsonUtils.pojoToJson(patched.getCustomProperties()),
|
||||
false);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -380,6 +389,11 @@ public class TypeResourceTest extends EntityResourceTest<Type, CreateType> {
|
||||
} else if (fieldName.contains("customPropertyConfig")) {
|
||||
String expectedStr = JsonUtils.pojoToJson(expected);
|
||||
String actualStr = JsonUtils.pojoToJson(actual);
|
||||
try {
|
||||
JSONAssert.assertEquals(expectedStr, actualStr, false);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
assertCommonFieldChange(fieldName, expected, actual);
|
||||
}
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/type/customProperties/complexTypes.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Basic",
|
||||
"description": "This schema defines custom properties complex types.",
|
||||
"definitions": {
|
||||
"entityReference": {
|
||||
"$comment": "@om-field-type",
|
||||
"description": "Entity Reference for Custom Property.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique identifier that identifies an entity instance.",
|
||||
"$ref": "../basic.json#/definitions/uuid"
|
||||
},
|
||||
"type": {
|
||||
"description": "Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`, `dashboardService`...",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the entity instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"fullyQualifiedName": {
|
||||
"description": "Fully qualified name of the entity instance. For entities such as tables, databases fullyQualifiedName is returned in this field. For entities that don't have name hierarchy such as `user` and `team` this will be same as the `name` field.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Optional description of entity.",
|
||||
"$ref": "../basic.json#/definitions/markdown"
|
||||
},
|
||||
"displayName": {
|
||||
"description": "Display Name that identifies this entity.",
|
||||
"type": "string"
|
||||
},
|
||||
"deleted": {
|
||||
"description": "If true the entity referred to has been soft-deleted.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"inherited": {
|
||||
"description": "If true the relationship indicated by this entity reference is inherited from the parent entity.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"href": {
|
||||
"description": "Link to the entity resource.",
|
||||
"$ref": "../basic.json#/definitions/href"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entityReferenceList": {
|
||||
"$comment": "@om-field-type",
|
||||
"description": "Entity Reference List for Custom Property.",
|
||||
"type": "array",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique identifier that identifies an entity instance.",
|
||||
"$ref": "../basic.json#/definitions/uuid"
|
||||
},
|
||||
"type": {
|
||||
"description": "Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`, `dashboardService`...",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the entity instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"fullyQualifiedName": {
|
||||
"description": "Fully qualified name of the entity instance. For entities such as tables, databases fullyQualifiedName is returned in this field. For entities that don't have name hierarchy such as `user` and `team` this will be same as the `name` field.",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Optional description of entity.",
|
||||
"$ref": "../basic.json#/definitions/markdown"
|
||||
},
|
||||
"displayName": {
|
||||
"description": "Display Name that identifies this entity.",
|
||||
"type": "string"
|
||||
},
|
||||
"deleted": {
|
||||
"description": "If true the entity referred to has been soft-deleted.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"inherited": {
|
||||
"description": "If true the relationship indicated by this entity reference is inherited from the parent entity.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"href": {
|
||||
"description": "Link to the entity resource.",
|
||||
"$ref": "../basic.json#/definitions/href"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,10 @@
|
||||
},
|
||||
"entityTypes": {
|
||||
"description": "Applies to Entity References. Entity Types can be used to restrict what type of entities can be configured for a entity reference.",
|
||||
"type": "string"
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"customPropertyConfig": {
|
||||
"type": "object",
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
import { uuid } from '../../constants/constants';
|
||||
import { CustomPropertySupportedEntityList } from '../../constants/CustomProperty.constant';
|
||||
import { EntityType } from '../../constants/Entity.interface';
|
||||
import { EntityType, ENTITY_PATH } from '../../constants/Entity.interface';
|
||||
import {
|
||||
createAnnouncement as createAnnouncementUtil,
|
||||
createInactiveAnnouncement as createInactiveAnnouncementUtil,
|
||||
@ -22,9 +22,7 @@ import {
|
||||
import {
|
||||
createCustomPropertyForEntity,
|
||||
CustomProperty,
|
||||
CustomPropertyType,
|
||||
deleteCustomPropertyForEntity,
|
||||
generateCustomProperty,
|
||||
deleteCustomProperties,
|
||||
setValueForProperty,
|
||||
validateValueForProperty,
|
||||
} from '../Utils/CustomProperty';
|
||||
@ -136,12 +134,8 @@ class EntityClass {
|
||||
endPoint: EntityType;
|
||||
protected name: string;
|
||||
|
||||
intergerPropertyDetails: CustomProperty;
|
||||
stringPropertyDetails: CustomProperty;
|
||||
markdownPropertyDetails: CustomProperty;
|
||||
|
||||
customPropertyValue: Record<
|
||||
CustomPropertyType,
|
||||
string,
|
||||
{ value: string; newValue: string; property: CustomProperty }
|
||||
>;
|
||||
|
||||
@ -153,34 +147,6 @@ class EntityClass {
|
||||
this.entityName = entityName;
|
||||
this.entityDetails = entityDetails;
|
||||
this.endPoint = endPoint;
|
||||
|
||||
this.intergerPropertyDetails = generateCustomProperty(
|
||||
CustomPropertyType.INTEGER
|
||||
);
|
||||
this.stringPropertyDetails = generateCustomProperty(
|
||||
CustomPropertyType.STRING
|
||||
);
|
||||
this.markdownPropertyDetails = generateCustomProperty(
|
||||
CustomPropertyType.MARKDOWN
|
||||
);
|
||||
|
||||
this.customPropertyValue = {
|
||||
Integer: {
|
||||
value: '123',
|
||||
newValue: '456',
|
||||
property: this.intergerPropertyDetails,
|
||||
},
|
||||
String: {
|
||||
value: '123',
|
||||
newValue: '456',
|
||||
property: this.stringPropertyDetails,
|
||||
},
|
||||
Markdown: {
|
||||
value: '**Bold statement**',
|
||||
newValue: '__Italic statement__',
|
||||
property: this.markdownPropertyDetails,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public getName() {
|
||||
@ -204,19 +170,15 @@ class EntityClass {
|
||||
|
||||
// Create custom property only for supported entities
|
||||
if (CustomPropertySupportedEntityList.includes(this.endPoint)) {
|
||||
createCustomPropertyForEntity({
|
||||
property: this.intergerPropertyDetails,
|
||||
type: this.endPoint,
|
||||
});
|
||||
|
||||
createCustomPropertyForEntity({
|
||||
property: this.stringPropertyDetails,
|
||||
type: this.endPoint,
|
||||
});
|
||||
|
||||
createCustomPropertyForEntity({
|
||||
property: this.markdownPropertyDetails,
|
||||
type: this.endPoint,
|
||||
createCustomPropertyForEntity(this.endPoint).then((data) => {
|
||||
this.customPropertyValue = data as unknown as Record<
|
||||
string,
|
||||
{
|
||||
value: string;
|
||||
newValue: string;
|
||||
property: CustomProperty;
|
||||
}
|
||||
>;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -280,17 +242,15 @@ class EntityClass {
|
||||
cleanup() {
|
||||
// Delete custom property only for supported entities
|
||||
if (CustomPropertySupportedEntityList.includes(this.endPoint)) {
|
||||
deleteCustomPropertyForEntity({
|
||||
property: this.intergerPropertyDetails,
|
||||
type: this.endPoint,
|
||||
});
|
||||
deleteCustomPropertyForEntity({
|
||||
property: this.stringPropertyDetails,
|
||||
type: this.endPoint,
|
||||
});
|
||||
deleteCustomPropertyForEntity({
|
||||
property: this.markdownPropertyDetails,
|
||||
type: this.endPoint,
|
||||
cy.getAllLocalStorage().then((data) => {
|
||||
const token = getToken(data);
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `/api/v1/metadata/types/name/${ENTITY_PATH[this.endPoint]}`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}).then(({ body }) => {
|
||||
deleteCustomProperties(body.id, token);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -520,13 +480,29 @@ class EntityClass {
|
||||
// Custom property
|
||||
|
||||
setCustomProperty(propertydetails: CustomProperty, value: string) {
|
||||
setValueForProperty(propertydetails.name, value);
|
||||
validateValueForProperty(propertydetails.name, value);
|
||||
setValueForProperty(
|
||||
propertydetails.name,
|
||||
value,
|
||||
propertydetails.propertyType.name
|
||||
);
|
||||
validateValueForProperty(
|
||||
propertydetails.name,
|
||||
value,
|
||||
propertydetails.propertyType.name
|
||||
);
|
||||
}
|
||||
|
||||
updateCustomProperty(propertydetails: CustomProperty, value: string) {
|
||||
setValueForProperty(propertydetails.name, value);
|
||||
validateValueForProperty(propertydetails.name, value);
|
||||
setValueForProperty(
|
||||
propertydetails.name,
|
||||
value,
|
||||
propertydetails.propertyType.name
|
||||
);
|
||||
validateValueForProperty(
|
||||
propertydetails.name,
|
||||
value,
|
||||
propertydetails.propertyType.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,24 +15,42 @@ import {
|
||||
CUSTOM_PROPERTY_INVALID_NAMES,
|
||||
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR,
|
||||
} from '../../constants/constants';
|
||||
import { EntityType } from '../../constants/Entity.interface';
|
||||
import { ENTITY_REFERENCE_PROPERTIES } from '../../constants/CustomProperty.constant';
|
||||
import { EntityType, ENTITY_PATH } from '../../constants/Entity.interface';
|
||||
import {
|
||||
descriptionBox,
|
||||
interceptURL,
|
||||
uuid,
|
||||
verifyResponseStatusCode,
|
||||
} from '../common';
|
||||
import { getToken } from './LocalStorage';
|
||||
|
||||
export enum CustomPropertyType {
|
||||
STRING = 'String',
|
||||
INTEGER = 'Integer',
|
||||
MARKDOWN = 'Markdown',
|
||||
}
|
||||
export enum CustomPropertyTypeByName {
|
||||
STRING = 'string',
|
||||
INTEGER = 'integer',
|
||||
MARKDOWN = 'markdown',
|
||||
NUMBER = 'number',
|
||||
DURATION = 'duration',
|
||||
EMAIL = 'email',
|
||||
ENUM = 'enum',
|
||||
SQL_QUERY = 'sqlQuery',
|
||||
TIMESTAMP = 'timestamp',
|
||||
TIME_INTERVAL = 'timeInterval',
|
||||
}
|
||||
|
||||
export interface CustomProperty {
|
||||
name: string;
|
||||
type: CustomPropertyType;
|
||||
description: string;
|
||||
propertyType: {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const generateCustomProperty = (type: CustomPropertyType) => ({
|
||||
@ -41,40 +59,67 @@ export const generateCustomProperty = (type: CustomPropertyType) => ({
|
||||
description: `${type} cypress Property`,
|
||||
});
|
||||
|
||||
export const createCustomPropertyForEntity = ({
|
||||
property,
|
||||
type,
|
||||
}: {
|
||||
property: CustomProperty;
|
||||
type: EntityType;
|
||||
}) => {
|
||||
interceptURL('GET', `/api/v1/metadata/types/name/*`, 'getEntity');
|
||||
export const getPropertyValues = (type: string) => {
|
||||
switch (type) {
|
||||
case 'integer':
|
||||
return {
|
||||
value: '123',
|
||||
newValue: '456',
|
||||
};
|
||||
case 'string':
|
||||
return {
|
||||
value: 'string value',
|
||||
newValue: 'new string value',
|
||||
};
|
||||
case 'markdown':
|
||||
return {
|
||||
value: '**Bold statement**',
|
||||
newValue: '__Italic statement__',
|
||||
};
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(type, true);
|
||||
case 'number':
|
||||
return {
|
||||
value: '123',
|
||||
newValue: '456',
|
||||
};
|
||||
case 'duration':
|
||||
return {
|
||||
value: 'PT1H',
|
||||
newValue: 'PT2H',
|
||||
};
|
||||
case 'email':
|
||||
return {
|
||||
value: 'john@gamil.com',
|
||||
newValue: 'user@getcollate.io',
|
||||
};
|
||||
case 'enum':
|
||||
return {
|
||||
value: 'small',
|
||||
newValue: 'medium',
|
||||
};
|
||||
case 'sqlQuery':
|
||||
return {
|
||||
value: 'Select * from table',
|
||||
newValue: 'Select * from table where id = 1',
|
||||
};
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
case 'timestamp':
|
||||
return {
|
||||
value: '1710831125922',
|
||||
newValue: '1710831125923',
|
||||
};
|
||||
case 'timeInterval':
|
||||
return {
|
||||
value: '1710831125922,1710831125923',
|
||||
newValue: '1710831125923,1710831125924',
|
||||
};
|
||||
|
||||
// Add Custom property for selected entity
|
||||
cy.get('[data-testid="add-field-button"]').click();
|
||||
|
||||
cy.get('[data-testid="name"]').clear().type(property.name);
|
||||
|
||||
cy.get('[data-testid="propertyType"]').click();
|
||||
cy.get(`[title="${property.type}"]`).click();
|
||||
|
||||
cy.get('.toastui-editor-md-container > .toastui-editor > .ProseMirror')
|
||||
.clear()
|
||||
.type(property.description);
|
||||
|
||||
// Check if the property got added
|
||||
cy.intercept('/api/v1/metadata/types/name/*?fields=customProperties').as(
|
||||
'customProperties'
|
||||
);
|
||||
cy.get('[data-testid="create-button"]').scrollIntoView().click();
|
||||
|
||||
cy.wait('@customProperties');
|
||||
cy.get('.ant-table-row').should('contain', property.name);
|
||||
default:
|
||||
return {
|
||||
value: '',
|
||||
newValue: '',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCustomPropertyForEntity = ({
|
||||
@ -102,7 +147,11 @@ export const deleteCustomPropertyForEntity = ({
|
||||
verifyResponseStatusCode('@patchEntity', 200);
|
||||
};
|
||||
|
||||
export const setValueForProperty = (propertyName, value: string) => {
|
||||
export const setValueForProperty = (
|
||||
propertyName: string,
|
||||
value: string,
|
||||
propertyType: string
|
||||
) => {
|
||||
cy.get('[data-testid="custom_properties"]').click();
|
||||
|
||||
cy.get('tbody').should('contain', propertyName);
|
||||
@ -119,28 +168,90 @@ export const setValueForProperty = (propertyName, value: string) => {
|
||||
|
||||
interceptURL('PATCH', `/api/v1/*/*`, 'patchEntity');
|
||||
// Checking for value text box or markdown box
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('[data-testid="value-input"]').length > 0) {
|
||||
cy.get('[data-testid="value-input"]').clear().type(value);
|
||||
cy.get('[data-testid="inline-save-btn"]').click();
|
||||
} else if (
|
||||
$body.find(
|
||||
'.toastui-editor-md-container > .toastui-editor > .ProseMirror'
|
||||
)
|
||||
) {
|
||||
|
||||
switch (propertyType) {
|
||||
case 'markdown':
|
||||
cy.get('.toastui-editor-md-container > .toastui-editor > .ProseMirror')
|
||||
.clear()
|
||||
.type(value);
|
||||
cy.get('[data-testid="save"]').click();
|
||||
|
||||
break;
|
||||
|
||||
case 'email':
|
||||
cy.get('[data-testid="email-input"]').clear().type(value);
|
||||
cy.get('[data-testid="inline-save-btn"]').click();
|
||||
|
||||
break;
|
||||
|
||||
case 'duration':
|
||||
cy.get('[data-testid="duration-input"]').clear().type(value);
|
||||
cy.get('[data-testid="inline-save-btn"]').click();
|
||||
|
||||
break;
|
||||
|
||||
case 'enum':
|
||||
cy.get('#enumValues').click().type(`${value}{enter}`);
|
||||
cy.clickOutside();
|
||||
cy.get('[data-testid="inline-save-btn"]').click();
|
||||
|
||||
break;
|
||||
|
||||
case 'sqlQuery':
|
||||
cy.get("pre[role='presentation']").last().click().type(value);
|
||||
cy.get('[data-testid="inline-save-btn"]').click();
|
||||
|
||||
break;
|
||||
|
||||
case 'timestamp':
|
||||
cy.get('[data-testid="timestamp-input"]').clear().type(value);
|
||||
cy.get('[data-testid="inline-save-btn"]').click();
|
||||
|
||||
break;
|
||||
|
||||
case 'timeInterval': {
|
||||
const [startValue, endValue] = value.split(',');
|
||||
cy.get('[data-testid="start-input"]').clear().type(startValue);
|
||||
cy.get('[data-testid="end-input"]').clear().type(endValue);
|
||||
cy.get('[data-testid="inline-save-btn"]').click();
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
case 'string':
|
||||
case 'integer':
|
||||
case 'number':
|
||||
cy.get('[data-testid="value-input"]').clear().type(value);
|
||||
cy.get('[data-testid="inline-save-btn"]').click();
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
verifyResponseStatusCode('@patchEntity', 200);
|
||||
cy.get(`[data-row-key="${propertyName}"]`).should(
|
||||
'contain',
|
||||
value.replace(/\*|_/gi, '')
|
||||
);
|
||||
|
||||
if (propertyType === 'enum') {
|
||||
cy.get('[data-testid="enum-value"]').should('contain', value);
|
||||
} else if (propertyType === 'timeInterval') {
|
||||
const [startValue, endValue] = value.split(',');
|
||||
cy.get('[data-testid="time-interval-value"]').should('contain', startValue);
|
||||
cy.get('[data-testid="time-interval-value"]').should('contain', endValue);
|
||||
} else if (propertyType === 'sqlQuery') {
|
||||
cy.get('.CodeMirror-scroll').should('contain', value);
|
||||
} else {
|
||||
cy.get(`[data-row-key="${propertyName}"]`).should(
|
||||
'contain',
|
||||
value.replace(/\*|_/gi, '')
|
||||
);
|
||||
}
|
||||
};
|
||||
export const validateValueForProperty = (propertyName, value: string) => {
|
||||
export const validateValueForProperty = (
|
||||
propertyName: string,
|
||||
value: string,
|
||||
propertyType: string
|
||||
) => {
|
||||
cy.get('.ant-tabs-tab').first().click();
|
||||
cy.get(
|
||||
'[data-testid="entity-right-panel"] [data-testid="custom-properties-table"]',
|
||||
@ -148,10 +259,21 @@ export const validateValueForProperty = (propertyName, value: string) => {
|
||||
timeout: 10000,
|
||||
}
|
||||
).scrollIntoView();
|
||||
cy.get(`[data-row-key="${propertyName}"]`).should(
|
||||
'contain',
|
||||
value.replace(/\*|_/gi, '')
|
||||
);
|
||||
|
||||
if (propertyType === 'enum') {
|
||||
cy.get('[data-testid="enum-value"]').should('contain', value);
|
||||
} else if (propertyType === 'timeInterval') {
|
||||
const [startValue, endValue] = value.split(',');
|
||||
cy.get('[data-testid="time-interval-value"]').should('contain', startValue);
|
||||
cy.get('[data-testid="time-interval-value"]').should('contain', endValue);
|
||||
} else if (propertyType === 'sqlQuery') {
|
||||
cy.get('.CodeMirror-scroll').should('contain', value);
|
||||
} else {
|
||||
cy.get(`[data-row-key="${propertyName}"]`).should(
|
||||
'contain',
|
||||
value.replace(/\*|_/gi, '')
|
||||
);
|
||||
}
|
||||
};
|
||||
export const generateCustomProperties = () => {
|
||||
return {
|
||||
@ -198,12 +320,21 @@ export const customPropertiesArray = Array(10)
|
||||
.fill(null)
|
||||
.map(() => generateCustomProperties());
|
||||
|
||||
export const addCustomPropertiesForEntity = (
|
||||
propertyName: string,
|
||||
customPropertyData: { description: string },
|
||||
customType: string,
|
||||
value: { values: string[]; multiSelect: boolean }
|
||||
) => {
|
||||
export const addCustomPropertiesForEntity = ({
|
||||
propertyName,
|
||||
customPropertyData,
|
||||
customType,
|
||||
enumConfig,
|
||||
formatConfig,
|
||||
entityReferenceConfig,
|
||||
}: {
|
||||
propertyName: string;
|
||||
customPropertyData: { description: string };
|
||||
customType: string;
|
||||
enumConfig?: { values: string[]; multiSelect: boolean };
|
||||
formatConfig?: string;
|
||||
entityReferenceConfig?: string[];
|
||||
}) => {
|
||||
// Add Custom property for selected entity
|
||||
cy.get('[data-testid="add-field-button"]').click();
|
||||
|
||||
@ -258,20 +389,35 @@ export const addCustomPropertiesForEntity = (
|
||||
|
||||
cy.get('[data-testid="name"]').clear().type(propertyName);
|
||||
|
||||
cy.get('[data-testid="propertyType"]').click();
|
||||
cy.get(`#root\\/propertyType`).clear().type(customType);
|
||||
cy.get(`[title="${customType}"]`).click();
|
||||
|
||||
if (customType === 'Enum') {
|
||||
value.values.forEach((val) => {
|
||||
cy.get('#root\\/customPropertyConfig').type(`${val}{enter}`);
|
||||
enumConfig.values.forEach((val) => {
|
||||
cy.get('#root\\/enumConfig').type(`${val}{enter}`);
|
||||
});
|
||||
|
||||
cy.clickOutside();
|
||||
|
||||
if (value.multiSelect) {
|
||||
if (enumConfig.multiSelect) {
|
||||
cy.get('#root\\/multiSelect').scrollIntoView().click();
|
||||
}
|
||||
}
|
||||
if (ENTITY_REFERENCE_PROPERTIES.includes(customType)) {
|
||||
entityReferenceConfig.forEach((val) => {
|
||||
cy.get('#root\\/entityReferenceConfig').click().type(`${val}`);
|
||||
cy.get(`[title="${val}"]`).click();
|
||||
});
|
||||
|
||||
cy.clickOutside();
|
||||
}
|
||||
|
||||
if (['Date', 'Date Time'].includes(customType)) {
|
||||
cy.get('#root\\/formatConfig').clear().type('invalid-format');
|
||||
cy.get('[role="alert"]').should('contain', 'Format is invalid');
|
||||
|
||||
cy.get('#root\\/formatConfig').clear().type(formatConfig);
|
||||
}
|
||||
|
||||
cy.get(descriptionBox).clear().type(customPropertyData.description);
|
||||
|
||||
@ -288,7 +434,7 @@ export const addCustomPropertiesForEntity = (
|
||||
cy.clickOnLogo();
|
||||
};
|
||||
|
||||
export const editCreatedProperty = (propertyName: string, type: string) => {
|
||||
export const editCreatedProperty = (propertyName: string, type?: string) => {
|
||||
// Fetching for edit button
|
||||
cy.get(`[data-row-key="${propertyName}"]`)
|
||||
.find('[data-testid="edit-button"]')
|
||||
@ -310,6 +456,12 @@ export const editCreatedProperty = (propertyName: string, type: string) => {
|
||||
cy.clickOutside();
|
||||
}
|
||||
|
||||
if (ENTITY_REFERENCE_PROPERTIES.includes(type)) {
|
||||
cy.get('#root\\/customPropertyConfig').click().type(`Table{enter}`);
|
||||
|
||||
cy.clickOutside();
|
||||
}
|
||||
|
||||
interceptURL('PATCH', '/api/v1/metadata/types/*', 'checkPatchForDescription');
|
||||
|
||||
cy.get('button[type="submit"]').scrollIntoView().click();
|
||||
@ -328,6 +480,11 @@ export const editCreatedProperty = (propertyName: string, type: string) => {
|
||||
.find('[data-testid="enum-config"]')
|
||||
.should('contain', '["enum1","enum2","enum3","updatedValue"]');
|
||||
}
|
||||
if (ENTITY_REFERENCE_PROPERTIES.includes(type)) {
|
||||
cy.get(`[data-row-key="${propertyName}"]`)
|
||||
.find(`[data-testid="${propertyName}-config"]`)
|
||||
.should('contain', '["user","team","table"]');
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCreatedProperty = (propertyName: string) => {
|
||||
@ -342,3 +499,84 @@ export const deleteCreatedProperty = (propertyName: string) => {
|
||||
|
||||
cy.get('[data-testid="save-button"]').should('be.visible').click();
|
||||
};
|
||||
|
||||
export const createCustomPropertyForEntity = (prop: string) => {
|
||||
return cy.getAllLocalStorage().then((data) => {
|
||||
const token = getToken(data);
|
||||
|
||||
// fetch the available property types
|
||||
return cy
|
||||
.request({
|
||||
method: 'GET',
|
||||
url: `/api/v1/metadata/types?category=field&limit=20`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then(({ body }) => {
|
||||
const propertyList = body.data.filter((item) =>
|
||||
Object.values(CustomPropertyTypeByName).includes(item.name)
|
||||
);
|
||||
|
||||
// fetch the entity details for which the custom property needs to be added
|
||||
return cy
|
||||
.request({
|
||||
method: 'GET',
|
||||
url: `/api/v1/metadata/types/name/${ENTITY_PATH[prop]}`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then(({ body }) => {
|
||||
const entityId = body.id;
|
||||
|
||||
// Add the custom property for the entity
|
||||
propertyList.forEach((item) => {
|
||||
return cy
|
||||
.request({
|
||||
method: 'PUT',
|
||||
url: `/api/v1/metadata/types/${entityId}`,
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
body: {
|
||||
name: `cyCustomProperty${uuid()}`,
|
||||
description: `cyCustomProperty${uuid()}`,
|
||||
propertyType: {
|
||||
id: item.id ?? '',
|
||||
type: 'type',
|
||||
},
|
||||
...(item.name === 'enum'
|
||||
? {
|
||||
customPropertyConfig: {
|
||||
config: {
|
||||
multiSelect: true,
|
||||
values: ['small', 'medium', 'large'],
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
.then(({ body }) => {
|
||||
return body.customProperties.reduce(
|
||||
(prev, curr) => {
|
||||
const propertyTypeName = curr.propertyType.name;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[propertyTypeName]: {
|
||||
...getPropertyValues(propertyTypeName),
|
||||
property: curr,
|
||||
},
|
||||
};
|
||||
},
|
||||
{} as Record<
|
||||
string,
|
||||
{
|
||||
value: string;
|
||||
newValue: string;
|
||||
property: CustomProperty;
|
||||
}
|
||||
>
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -25,3 +25,8 @@ export const CustomPropertySupportedEntityList = [
|
||||
EntityType.GlossaryTerm,
|
||||
EntityType.SearchIndex,
|
||||
];
|
||||
|
||||
export const ENTITY_REFERENCE_PROPERTIES = [
|
||||
'Entity Reference',
|
||||
'Entity Reference List',
|
||||
];
|
||||
|
||||
@ -110,3 +110,19 @@ export enum SidebarItem {
|
||||
SETTINGS = 'settings',
|
||||
LOGOUT = 'logout',
|
||||
}
|
||||
|
||||
export enum ENTITY_PATH {
|
||||
tables = 'table',
|
||||
topics = 'topic',
|
||||
dashboards = 'dashboard',
|
||||
pipelines = 'pipeline',
|
||||
mlmodels = 'mlmodel',
|
||||
containers = 'container',
|
||||
tags = 'tag',
|
||||
glossaries = 'glossary',
|
||||
searchIndexes = 'searchIndex',
|
||||
storedProcedures = 'storedProcedure',
|
||||
glossaryTerm = 'glossaryTerm',
|
||||
databases = 'database',
|
||||
databaseSchemas = 'databaseSchema',
|
||||
}
|
||||
|
||||
@ -491,42 +491,108 @@ export const ENTITY_SERVICE_TYPE = {
|
||||
};
|
||||
|
||||
export const ENTITIES = {
|
||||
entity_table: {
|
||||
name: 'table',
|
||||
description: 'This is Table custom property',
|
||||
integerValue: '45',
|
||||
entity_container: {
|
||||
name: 'container',
|
||||
description: 'This is Container custom property',
|
||||
integerValue: '14',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
entityObj: SEARCH_ENTITY_TABLE.table_1,
|
||||
entityApiType: 'tables',
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: {},
|
||||
entityApiType: 'containers',
|
||||
},
|
||||
entity_topic: {
|
||||
name: 'topic',
|
||||
description: 'This is Topic custom property',
|
||||
integerValue: '23',
|
||||
|
||||
entity_dashboard: {
|
||||
name: 'dashboard',
|
||||
description: 'This is Dashboard custom property',
|
||||
integerValue: '14',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
entityObj: SEARCH_ENTITY_TOPIC.topic_1,
|
||||
entityApiType: 'topics',
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: SEARCH_ENTITY_DASHBOARD.dashboard_1,
|
||||
entityApiType: 'dashboards',
|
||||
},
|
||||
// commenting the dashboard test for not, need to make changes in dynamic data-test side
|
||||
// entity_dashboard: {
|
||||
// name: 'dashboard',
|
||||
// description: 'This is Dashboard custom property',
|
||||
// integerValue: '14',
|
||||
// stringValue: 'This is string propery',
|
||||
// markdownValue: 'This is markdown value',
|
||||
// entityObj: SEARCH_ENTITY_DASHBOARD.dashboard_1,
|
||||
// entityApiType: "dashboards"
|
||||
// },
|
||||
|
||||
entity_database: {
|
||||
name: 'database',
|
||||
description: 'This is Database custom property',
|
||||
integerValue: '14',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: {},
|
||||
entityApiType: 'databases',
|
||||
},
|
||||
|
||||
entity_databaseSchema: {
|
||||
name: 'databaseSchema',
|
||||
description: 'This is Database Schema custom property',
|
||||
integerValue: '14',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: {},
|
||||
entityApiType: 'databaseSchemas',
|
||||
},
|
||||
|
||||
entity_glossaryTerm: {
|
||||
name: 'glossaryTerm',
|
||||
description: 'This is Glossary Term custom property',
|
||||
integerValue: '14',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: {},
|
||||
entityApiType: 'glossaryTerm',
|
||||
},
|
||||
|
||||
entity_mlmodel: {
|
||||
name: 'mlmodel',
|
||||
description: 'This is ML Model custom property',
|
||||
integerValue: '14',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: {},
|
||||
entityApiType: 'mlmodels',
|
||||
},
|
||||
|
||||
entity_pipeline: {
|
||||
name: 'pipeline',
|
||||
description: 'This is Pipeline custom property',
|
||||
@ -537,9 +603,80 @@ export const ENTITIES = {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: true,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: SEARCH_ENTITY_PIPELINE.pipeline_1,
|
||||
entityApiType: 'pipelines',
|
||||
},
|
||||
|
||||
entity_searchIndex: {
|
||||
name: 'searchIndex',
|
||||
description: 'This is Search Index custom property',
|
||||
integerValue: '14',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: {},
|
||||
entityApiType: 'searchIndexes',
|
||||
},
|
||||
|
||||
entity_storedProcedure: {
|
||||
name: 'storedProcedure',
|
||||
description: 'This is Stored Procedure custom property',
|
||||
integerValue: '14',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: {},
|
||||
entityApiType: 'storedProcedures',
|
||||
},
|
||||
|
||||
entity_table: {
|
||||
name: 'table',
|
||||
description: 'This is Table custom property',
|
||||
integerValue: '45',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: SEARCH_ENTITY_TABLE.table_1,
|
||||
entityApiType: 'tables',
|
||||
},
|
||||
|
||||
entity_topic: {
|
||||
name: 'topic',
|
||||
description: 'This is Topic custom property',
|
||||
integerValue: '23',
|
||||
stringValue: 'This is string propery',
|
||||
markdownValue: 'This is markdown value',
|
||||
enumConfig: {
|
||||
values: ['enum1', 'enum2', 'enum3'],
|
||||
multiSelect: false,
|
||||
},
|
||||
dateFormatConfig: 'yyyy-mm-dd',
|
||||
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
|
||||
entityReferenceConfig: ['User', 'Team'],
|
||||
entityObj: SEARCH_ENTITY_TOPIC.topic_1,
|
||||
entityApiType: 'topics',
|
||||
},
|
||||
};
|
||||
|
||||
export const LOGIN = {
|
||||
|
||||
@ -21,6 +21,7 @@ import { deleteGlossary } from '../../common/GlossaryUtils';
|
||||
import {
|
||||
addCustomPropertiesForEntity,
|
||||
customPropertiesArray,
|
||||
CustomProperty,
|
||||
CustomPropertyType,
|
||||
deleteCreatedProperty,
|
||||
deleteCustomProperties,
|
||||
@ -37,7 +38,6 @@ import {
|
||||
} from '../../common/Utils/Entity';
|
||||
import { getToken } from '../../common/Utils/LocalStorage';
|
||||
import {
|
||||
DATA_ASSETS,
|
||||
ENTITIES,
|
||||
INVALID_NAMES,
|
||||
NAME_MAX_LENGTH_VALIDATION_ERROR,
|
||||
@ -46,7 +46,7 @@ import {
|
||||
NEW_GLOSSARY_TERMS,
|
||||
uuid,
|
||||
} from '../../constants/constants';
|
||||
import { SidebarItem } from '../../constants/Entity.interface';
|
||||
import { EntityType, SidebarItem } from '../../constants/Entity.interface';
|
||||
import { DATABASE_SERVICE } from '../../constants/EntityConstant';
|
||||
|
||||
const CREDENTIALS = {
|
||||
@ -282,180 +282,71 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
describe('Add update and delete Integer custom properties', () => {
|
||||
Object.values(ENTITIES).forEach((entity) => {
|
||||
const propertyName = `addcyentity${entity.name}test${uuid()}`;
|
||||
[
|
||||
'Integer',
|
||||
'String',
|
||||
'Markdown',
|
||||
'Duration',
|
||||
'Email',
|
||||
'Number',
|
||||
'Sql Query',
|
||||
'Time',
|
||||
'Time Interval',
|
||||
'Timestamp',
|
||||
].forEach((type) => {
|
||||
describe(`Add update and delete ${type} custom properties`, () => {
|
||||
Object.values(ENTITIES).forEach((entity) => {
|
||||
const propertyName = `addcyentity${entity.name}test${uuid()}`;
|
||||
|
||||
it(`Add Integer custom property for ${entity.name} Entities`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
it(`Add ${type} custom property for ${entity.name} Entities`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
|
||||
// Getting the property
|
||||
addCustomPropertiesForEntity(
|
||||
propertyName,
|
||||
entity,
|
||||
'Integer',
|
||||
entity.integerValue,
|
||||
entity.entityObj
|
||||
);
|
||||
// Getting the property
|
||||
addCustomPropertiesForEntity({
|
||||
propertyName,
|
||||
customPropertyData: entity,
|
||||
customType: type,
|
||||
});
|
||||
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
});
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
});
|
||||
|
||||
it(`Edit created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
it(`Edit created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
editCreatedProperty(propertyName);
|
||||
});
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
editCreatedProperty(propertyName);
|
||||
});
|
||||
|
||||
it(`Delete created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
it(`Delete created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
deleteCreatedProperty(propertyName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add update and delete String custom properties', () => {
|
||||
Object.values(ENTITIES).forEach((entity) => {
|
||||
const propertyName = `addcyentity${entity.name}test${uuid()}`;
|
||||
|
||||
it(`Add String custom property for ${entity.name} Entities`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
|
||||
addCustomPropertiesForEntity(
|
||||
propertyName,
|
||||
entity,
|
||||
'String',
|
||||
entity.stringValue,
|
||||
entity.entityObj
|
||||
);
|
||||
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
});
|
||||
|
||||
it(`Edit created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
editCreatedProperty(propertyName);
|
||||
});
|
||||
|
||||
it(`Delete created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
deleteCreatedProperty(propertyName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add update and delete Markdown custom properties', () => {
|
||||
Object.values(ENTITIES).forEach((entity) => {
|
||||
const propertyName = `addcyentity${entity.name}test${uuid()}`;
|
||||
|
||||
it(`Add Markdown custom property for ${entity.name} Entities`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
|
||||
addCustomPropertiesForEntity(
|
||||
propertyName,
|
||||
entity,
|
||||
'Markdown',
|
||||
entity.markdownValue,
|
||||
entity.entityObj
|
||||
);
|
||||
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
});
|
||||
|
||||
it(`Edit created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
editCreatedProperty(propertyName);
|
||||
});
|
||||
|
||||
it(`Delete created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
deleteCreatedProperty(propertyName);
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
deleteCreatedProperty(propertyName);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -476,13 +367,12 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => {
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
|
||||
addCustomPropertiesForEntity(
|
||||
addCustomPropertiesForEntity({
|
||||
propertyName,
|
||||
entity,
|
||||
'Enum',
|
||||
entity.enumConfig,
|
||||
entity.entityObj
|
||||
);
|
||||
customPropertyData: entity,
|
||||
customType: 'Enum',
|
||||
enumConfig: entity.enumConfig,
|
||||
});
|
||||
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
@ -520,6 +410,242 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add update and delete Entity Reference custom properties', () => {
|
||||
Object.values(ENTITIES).forEach((entity) => {
|
||||
const propertyName = `addcyentity${entity.name}test${uuid()}`;
|
||||
|
||||
it(`Add Entity Reference custom property for ${entity.name} Entities`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
|
||||
addCustomPropertiesForEntity({
|
||||
propertyName,
|
||||
customPropertyData: entity,
|
||||
customType: 'Entity Reference',
|
||||
entityReferenceConfig: entity.entityReferenceConfig,
|
||||
});
|
||||
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
});
|
||||
|
||||
it(`Edit created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
editCreatedProperty(propertyName, 'Entity Reference');
|
||||
});
|
||||
|
||||
it(`Delete created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
deleteCreatedProperty(propertyName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add update and delete Entity Reference List custom properties', () => {
|
||||
Object.values(ENTITIES).forEach((entity) => {
|
||||
const propertyName = `addcyentity${entity.name}test${uuid()}`;
|
||||
|
||||
it(`Add Entity Reference List custom property for ${entity.name} Entities`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
|
||||
addCustomPropertiesForEntity({
|
||||
propertyName,
|
||||
customPropertyData: entity,
|
||||
customType: 'Entity Reference List',
|
||||
entityReferenceConfig: entity.entityReferenceConfig,
|
||||
});
|
||||
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
});
|
||||
|
||||
it(`Edit created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
editCreatedProperty(propertyName, 'Entity Reference List');
|
||||
});
|
||||
|
||||
it(`Delete created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
deleteCreatedProperty(propertyName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add update and delete Date custom properties', () => {
|
||||
Object.values(ENTITIES).forEach((entity) => {
|
||||
const propertyName = `addcyentity${entity.name}test${uuid()}`;
|
||||
|
||||
it(`Add Date custom property for ${entity.name} Entities`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
|
||||
addCustomPropertiesForEntity({
|
||||
propertyName,
|
||||
customPropertyData: entity,
|
||||
customType: 'Date',
|
||||
formatConfig: entity.dateFormatConfig,
|
||||
});
|
||||
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
});
|
||||
|
||||
it(`Edit created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
editCreatedProperty(propertyName);
|
||||
});
|
||||
|
||||
it(`Delete created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
deleteCreatedProperty(propertyName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add update and delete DateTime custom properties', () => {
|
||||
Object.values(ENTITIES).forEach((entity) => {
|
||||
const propertyName = `addcyentity${entity.name}test${uuid()}`;
|
||||
|
||||
it(`Add DateTime custom property for ${entity.name} Entities`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
|
||||
addCustomPropertiesForEntity({
|
||||
propertyName,
|
||||
customPropertyData: entity,
|
||||
customType: 'Date Time',
|
||||
formatConfig: entity.dateTimeFormatConfig,
|
||||
});
|
||||
|
||||
// Navigating back to custom properties page
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
});
|
||||
|
||||
it(`Edit created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
editCreatedProperty(propertyName);
|
||||
});
|
||||
|
||||
it(`Delete created property for ${entity.name} entity`, () => {
|
||||
interceptURL(
|
||||
'GET',
|
||||
`/api/v1/metadata/types/name/${entity.name}*`,
|
||||
'getEntity'
|
||||
);
|
||||
|
||||
// Selecting the entity
|
||||
cy.settingClick(entity.entityApiType, true);
|
||||
|
||||
verifyResponseStatusCode('@getEntity', 200);
|
||||
deleteCreatedProperty(propertyName);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom properties for glossary and glossary terms', () => {
|
||||
const propertyName = `addcyentity${glossaryTerm.name}test${uuid()}`;
|
||||
const properties = Object.values(CustomPropertyType).join(', ');
|
||||
@ -527,13 +653,11 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => {
|
||||
it('test custom properties in advanced search modal', () => {
|
||||
cy.settingClick(glossaryTerm.entityApiType, true);
|
||||
|
||||
addCustomPropertiesForEntity(
|
||||
addCustomPropertiesForEntity({
|
||||
propertyName,
|
||||
glossaryTerm,
|
||||
'Integer',
|
||||
'45',
|
||||
null
|
||||
);
|
||||
customPropertyData: glossaryTerm,
|
||||
customType: 'Integer',
|
||||
});
|
||||
|
||||
// Navigating to explore page
|
||||
cy.sidebarClick(SidebarItem.EXPLORE);
|
||||
@ -593,29 +717,33 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => {
|
||||
cy.settingClick(glossaryTerm.entityApiType, true);
|
||||
|
||||
Object.values(CustomPropertyType).forEach((type) => {
|
||||
addCustomPropertiesForEntity(
|
||||
lowerCase(type),
|
||||
glossaryTerm,
|
||||
type,
|
||||
`${type}-(${uuid()})`,
|
||||
null
|
||||
);
|
||||
addCustomPropertiesForEntity({
|
||||
propertyName: lowerCase(type),
|
||||
customPropertyData: glossaryTerm,
|
||||
customType: type,
|
||||
});
|
||||
|
||||
cy.settingClick(glossaryTerm.entityApiType, true);
|
||||
});
|
||||
|
||||
visitEntityDetailsPage({
|
||||
term: NEW_GLOSSARY_TERMS.term_1.name,
|
||||
serviceName: NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName,
|
||||
entity: 'glossaryTerms',
|
||||
entity: 'glossaryTerms' as EntityType,
|
||||
dataTestId: 'Cypress Glossary-CypressPurchase',
|
||||
});
|
||||
|
||||
// set custom property value
|
||||
Object.values(CustomPropertyType).forEach((type) => {
|
||||
setValueForProperty(lowerCase(type), customPropertyValue[type].value);
|
||||
setValueForProperty(
|
||||
lowerCase(type),
|
||||
customPropertyValue[type].value,
|
||||
lowerCase(type)
|
||||
);
|
||||
validateValueForProperty(
|
||||
lowerCase(type),
|
||||
customPropertyValue[type].value
|
||||
customPropertyValue[type].value,
|
||||
lowerCase(type)
|
||||
);
|
||||
});
|
||||
|
||||
@ -623,21 +751,26 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => {
|
||||
Object.values(CustomPropertyType).forEach((type) => {
|
||||
setValueForProperty(
|
||||
lowerCase(type),
|
||||
customPropertyValue[type].newValue
|
||||
customPropertyValue[type].newValue,
|
||||
lowerCase(type)
|
||||
);
|
||||
validateValueForProperty(
|
||||
lowerCase(type),
|
||||
customPropertyValue[type].newValue
|
||||
customPropertyValue[type].newValue,
|
||||
lowerCase(type)
|
||||
);
|
||||
});
|
||||
|
||||
// delete custom properties
|
||||
Object.values(CustomPropertyType).forEach((customPropertyType) => {
|
||||
const type = glossaryTerm.entityApiType;
|
||||
const type = glossaryTerm.entityApiType as EntityType;
|
||||
const property = customPropertyValue[customPropertyType].property ?? {};
|
||||
|
||||
deleteCustomPropertyForEntity({
|
||||
property: { ...property, name: lowerCase(customPropertyType) },
|
||||
property: {
|
||||
...property,
|
||||
name: lowerCase(customPropertyType),
|
||||
} as CustomProperty,
|
||||
type,
|
||||
});
|
||||
});
|
||||
@ -699,7 +832,7 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => {
|
||||
visitEntityDetailsPage({
|
||||
term: DATABASE_SERVICE.entity.name,
|
||||
serviceName: DATABASE_SERVICE.service.name,
|
||||
entity: DATA_ASSETS.tables,
|
||||
entity: EntityType.Table,
|
||||
});
|
||||
verifyCustomPropertyRows();
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import EntityClass from '../../common/Entities/EntityClass';
|
||||
import { CustomPropertyType } from '../../common/Utils/CustomProperty';
|
||||
import { CustomPropertyTypeByName } from '../../common/Utils/CustomProperty';
|
||||
import DatabaseClass from './../../common/Entities/DatabaseClass';
|
||||
import DatabaseSchemaClass from './../../common/Entities/DatabaseSchemaClass';
|
||||
import StoreProcedureClass from './../../common/Entities/StoredProcedureClass';
|
||||
@ -111,7 +111,7 @@ describe('Database hierarchy details page', { tags: 'DataAssets' }, () => {
|
||||
entity.removeInactiveAnnouncement();
|
||||
});
|
||||
|
||||
Object.values(CustomPropertyType).forEach((type) => {
|
||||
Object.values(CustomPropertyTypeByName).forEach((type) => {
|
||||
it(`Set ${type} Custom Property `, () => {
|
||||
entity.setCustomProperty(
|
||||
entity.customPropertyValue[type].property,
|
||||
|
||||
@ -11,16 +11,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ContainerClass from '../../common/Entities/ContainerClass';
|
||||
import DashboardDataModelClass from '../../common/Entities/DataModelClass';
|
||||
import EntityClass from '../../common/Entities/EntityClass';
|
||||
import MlModelClass from '../../common/Entities/MlModelClass';
|
||||
import PipelineClass from '../../common/Entities/PipelineClass';
|
||||
import SearchIndexClass from '../../common/Entities/SearchIndexClass';
|
||||
import TopicClass from '../../common/Entities/TopicClass';
|
||||
import { CustomPropertySupportedEntityList } from '../../constants/CustomProperty.constant';
|
||||
import ContainerClass from './../../common/Entities/ContainerClass';
|
||||
import DashboardClass from './../../common/Entities/DashboardClass';
|
||||
import DashboardDataModelClass from './../../common/Entities/DataModelClass';
|
||||
import MlModelClass from './../../common/Entities/MlModelClass';
|
||||
import PipelineClass from './../../common/Entities/PipelineClass';
|
||||
import SearchIndexClass from './../../common/Entities/SearchIndexClass';
|
||||
import TopicClass from './../../common/Entities/TopicClass';
|
||||
import { CustomPropertyType } from './../../common/Utils/CustomProperty';
|
||||
import { CustomPropertyTypeByName } from './../../common/Utils/CustomProperty';
|
||||
|
||||
// Run tests over all entities except Database, Schema, Table and Store Procedure
|
||||
// Those tests are covered in cypress/new-tests/Database.spec.js
|
||||
@ -122,10 +122,10 @@ describe('Entity detail page', { tags: 'DataAssets' }, () => {
|
||||
|
||||
// Create custom property only for supported entities
|
||||
if (CustomPropertySupportedEntityList.includes(entity.endPoint)) {
|
||||
const properties = Object.values(CustomPropertyType).join(', ');
|
||||
const properties = Object.values(CustomPropertyTypeByName).join(', ');
|
||||
|
||||
it(`Set ${properties} Custom Property `, () => {
|
||||
Object.values(CustomPropertyType).forEach((type) => {
|
||||
Object.values(CustomPropertyTypeByName).forEach((type) => {
|
||||
entity.setCustomProperty(
|
||||
entity.customPropertyValue[type].property,
|
||||
entity.customPropertyValue[type].value
|
||||
@ -134,7 +134,7 @@ describe('Entity detail page', { tags: 'DataAssets' }, () => {
|
||||
});
|
||||
|
||||
it(`Update ${properties} Custom Property`, () => {
|
||||
Object.values(CustomPropertyType).forEach((type) => {
|
||||
Object.values(CustomPropertyTypeByName).forEach((type) => {
|
||||
entity.updateCustomProperty(
|
||||
entity.customPropertyValue[type].property,
|
||||
entity.customPropertyValue[type].newValue
|
||||
|
||||
@ -21,7 +21,7 @@ Describe your custom property to provide more information to your team.
|
||||
$$
|
||||
|
||||
$$section
|
||||
### Enum Values $(id="customPropertyConfig")
|
||||
### Enum Values $(id="enumConfig")
|
||||
|
||||
Add the list of values for enum property.
|
||||
$$
|
||||
@ -30,4 +30,16 @@ $$section
|
||||
### Multi Select $(id="multiSelect")
|
||||
|
||||
Enable multi select of values for enum property.
|
||||
$$
|
||||
|
||||
$$section
|
||||
### Format $(id="formatConfig")
|
||||
|
||||
To specify a format for the `date` or `dateTime` type, example you can use the following pattern: `dd-mm-yyyy` or `dd-mm-yyyy HH:mm:ss`.
|
||||
$$
|
||||
|
||||
$$section
|
||||
### Entity Reference type $(id="entityReferenceConfig")
|
||||
|
||||
Select the reference type for your custom property value type.
|
||||
$$
|
||||
@ -125,11 +125,16 @@ const DataAssetAsyncSelectList: FC<DataAssetAsyncSelectListProps> = ({
|
||||
const { value, reference, displayName } = option;
|
||||
|
||||
let label;
|
||||
if (searchIndex === SearchIndex.USER) {
|
||||
if (
|
||||
searchIndex === SearchIndex.USER ||
|
||||
searchIndex.includes('user') ||
|
||||
searchIndex.includes('team')
|
||||
) {
|
||||
label = (
|
||||
<Space>
|
||||
<ProfilePicture
|
||||
className="d-flex"
|
||||
isTeam={reference.type === EntityType.TEAM}
|
||||
name={option.name ?? ''}
|
||||
type="circle"
|
||||
width="24"
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
import { Button, Col, Form, Row } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { t } from 'i18next';
|
||||
import { isUndefined, map, omit, startCase } from 'lodash';
|
||||
import { isUndefined, map, omit, omitBy, startCase } from 'lodash';
|
||||
import React, {
|
||||
FocusEvent,
|
||||
useCallback,
|
||||
@ -23,7 +23,11 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { SUPPORTED_FIELD_TYPES } from '../../../../constants/constants';
|
||||
import {
|
||||
ENTITY_REFERENCE_OPTIONS,
|
||||
PROPERTY_TYPES_WITH_ENTITY_REFERENCE,
|
||||
PROPERTY_TYPES_WITH_FORMAT,
|
||||
} from '../../../../constants/CustomProperty.constants';
|
||||
import { GlobalSettingsMenuCategory } from '../../../../constants/GlobalSettings.constants';
|
||||
import { CUSTOM_PROPERTY_NAME_REGEX } from '../../../../constants/regex.constants';
|
||||
import {
|
||||
@ -44,6 +48,7 @@ import {
|
||||
getTypeByFQN,
|
||||
getTypeListByCategory,
|
||||
} from '../../../../rest/metadataTypeAPI';
|
||||
import { isValidDateFormat } from '../../../../utils/date-time/DateTimeUtils';
|
||||
import { generateFormFields } from '../../../../utils/formUtils';
|
||||
import { getSettingOptionByEntityType } from '../../../../utils/GlobalSettingsUtils';
|
||||
import { getSettingPath } from '../../../../utils/RouterUtils';
|
||||
@ -89,20 +94,34 @@ const AddCustomProperty = () => {
|
||||
);
|
||||
|
||||
const propertyTypeOptions = useMemo(() => {
|
||||
const supportedTypes = propertyTypes.filter((property) =>
|
||||
SUPPORTED_FIELD_TYPES.includes(property.name)
|
||||
);
|
||||
|
||||
return map(supportedTypes, (type) => ({
|
||||
return map(propertyTypes, (type) => ({
|
||||
key: type.name,
|
||||
label: startCase(type.displayName ?? type.name),
|
||||
value: type.id,
|
||||
}));
|
||||
}, [propertyTypes]);
|
||||
|
||||
const isEnumType =
|
||||
propertyTypeOptions.find((option) => option.value === watchedPropertyType)
|
||||
?.key === 'enum';
|
||||
const { hasEnumConfig, hasFormatConfig, hasEntityReferenceConfig } =
|
||||
useMemo(() => {
|
||||
const watchedOption = propertyTypeOptions.find(
|
||||
(option) => option.value === watchedPropertyType
|
||||
);
|
||||
const watchedOptionKey = watchedOption?.key ?? '';
|
||||
|
||||
const hasEnumConfig = watchedOptionKey === 'enum';
|
||||
|
||||
const hasFormatConfig =
|
||||
PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey);
|
||||
|
||||
const hasEntityReferenceConfig =
|
||||
PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(watchedOptionKey);
|
||||
|
||||
return {
|
||||
hasEnumConfig,
|
||||
hasFormatConfig,
|
||||
hasEntityReferenceConfig,
|
||||
};
|
||||
}, [watchedPropertyType, propertyTypeOptions]);
|
||||
|
||||
const fetchPropertyType = async () => {
|
||||
try {
|
||||
@ -135,13 +154,11 @@ const AddCustomProperty = () => {
|
||||
* In CustomProperty the propertyType is type of entity reference, however from the form we
|
||||
* get propertyType as string
|
||||
*/
|
||||
/**
|
||||
* In CustomProperty the customPropertyConfig is type of CustomPropertyConfig, however from the
|
||||
* form we get customPropertyConfig as string[]
|
||||
*/
|
||||
data: Exclude<CustomProperty, 'propertyType' | 'customPropertyConfig'> & {
|
||||
data: Exclude<CustomProperty, 'propertyType'> & {
|
||||
propertyType: string;
|
||||
customPropertyConfig: string[];
|
||||
enumConfig: string[];
|
||||
formatConfig: string;
|
||||
entityReferenceConfig: string[];
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
) => {
|
||||
@ -151,24 +168,47 @@ const AddCustomProperty = () => {
|
||||
|
||||
try {
|
||||
setIsCreating(true);
|
||||
await addPropertyToEntity(typeDetail?.id ?? '', {
|
||||
...omit(data, 'multiSelect'),
|
||||
propertyType: {
|
||||
id: data.propertyType,
|
||||
type: 'type',
|
||||
let customPropertyConfig;
|
||||
|
||||
if (hasEnumConfig) {
|
||||
customPropertyConfig = {
|
||||
config: {
|
||||
multiSelect: Boolean(data?.multiSelect),
|
||||
values: data.enumConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (hasFormatConfig) {
|
||||
customPropertyConfig = {
|
||||
config: data.formatConfig,
|
||||
};
|
||||
}
|
||||
|
||||
if (hasEntityReferenceConfig) {
|
||||
customPropertyConfig = {
|
||||
config: data.entityReferenceConfig,
|
||||
};
|
||||
}
|
||||
|
||||
const payload = omitBy(
|
||||
{
|
||||
...omit(data, [
|
||||
'multiSelect',
|
||||
'formatConfig',
|
||||
'entityReferenceConfig',
|
||||
'enumConfig',
|
||||
]),
|
||||
propertyType: {
|
||||
id: data.propertyType,
|
||||
type: 'type',
|
||||
},
|
||||
customPropertyConfig,
|
||||
},
|
||||
// Only add customPropertyConfig if it is an enum type
|
||||
...(isEnumType
|
||||
? {
|
||||
customPropertyConfig: {
|
||||
config: {
|
||||
multiSelect: Boolean(data?.multiSelect),
|
||||
values: data.customPropertyConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
isUndefined
|
||||
) as unknown as CustomProperty;
|
||||
|
||||
await addPropertyToEntity(typeDetail?.id ?? '', payload);
|
||||
history.goBack();
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
@ -216,6 +256,12 @@ const AddCustomProperty = () => {
|
||||
placeholder: `${t('label.select-field', {
|
||||
field: t('label.type'),
|
||||
})}`,
|
||||
showSearch: true,
|
||||
filterOption: (input: string, option: { label: string }) => {
|
||||
return (option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase());
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -232,14 +278,14 @@ const AddCustomProperty = () => {
|
||||
},
|
||||
};
|
||||
|
||||
const customPropertyConfigTypeValueField: FieldProp = {
|
||||
name: 'customPropertyConfig',
|
||||
const enumConfigField: FieldProp = {
|
||||
name: 'enumConfig',
|
||||
required: false,
|
||||
label: t('label.enum-value-plural'),
|
||||
id: 'root/customPropertyConfig',
|
||||
id: 'root/enumConfig',
|
||||
type: FieldTypes.SELECT,
|
||||
props: {
|
||||
'data-testid': 'customPropertyConfig',
|
||||
'data-testid': 'enumConfig',
|
||||
mode: 'tags',
|
||||
placeholder: t('label.enum-value-plural'),
|
||||
},
|
||||
@ -265,6 +311,50 @@ const AddCustomProperty = () => {
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
};
|
||||
|
||||
const formatConfigField: FieldProp = {
|
||||
name: 'formatConfig',
|
||||
required: false,
|
||||
label: t('label.format'),
|
||||
id: 'root/formatConfig',
|
||||
type: FieldTypes.TEXT,
|
||||
props: {
|
||||
'data-testid': 'formatConfig',
|
||||
autoComplete: 'off',
|
||||
},
|
||||
placeholder: t('label.format'),
|
||||
rules: [
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (!isValidDateFormat(value)) {
|
||||
return Promise.reject(
|
||||
t('label.field-invalid', {
|
||||
field: t('label.format'),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const entityReferenceConfigField: FieldProp = {
|
||||
name: 'entityReferenceConfig',
|
||||
required: true,
|
||||
label: t('label.entity-reference-types'),
|
||||
id: 'root/entityReferenceConfig',
|
||||
type: FieldTypes.SELECT,
|
||||
props: {
|
||||
mode: 'multiple',
|
||||
options: ENTITY_REFERENCE_OPTIONS,
|
||||
'data-testid': 'entityReferenceConfig',
|
||||
placeholder: `${t('label.select-field', {
|
||||
field: t('label.type'),
|
||||
})}`,
|
||||
},
|
||||
};
|
||||
|
||||
const firstPanelChildren = (
|
||||
<div className="max-width-md w-9/10 service-form-container">
|
||||
<TitleBreadcrumb titleLinks={slashedBreadcrumb} />
|
||||
@ -276,14 +366,21 @@ const AddCustomProperty = () => {
|
||||
onFinish={handleSubmit}
|
||||
onFocus={handleFieldFocus}>
|
||||
{generateFormFields(formFields)}
|
||||
{isEnumType && (
|
||||
<>
|
||||
{generateFormFields([
|
||||
customPropertyConfigTypeValueField,
|
||||
multiSelectField,
|
||||
])}
|
||||
</>
|
||||
)}
|
||||
{
|
||||
// Only show enum value field if the property type has enum config
|
||||
hasEnumConfig &&
|
||||
generateFormFields([enumConfigField, multiSelectField])
|
||||
}
|
||||
{
|
||||
// Only show format field if the property type has format config
|
||||
hasFormatConfig && generateFormFields([formatConfigField])
|
||||
}
|
||||
|
||||
{
|
||||
// Only show entity reference field if the property type has entity reference config
|
||||
hasEntityReferenceConfig &&
|
||||
generateFormFields([entityReferenceConfigField])
|
||||
}
|
||||
{generateFormFields([descriptionField])}
|
||||
<Row justify="end">
|
||||
<Col>
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
*/
|
||||
import { Button, Space, Tooltip, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { isEmpty, isString, isUndefined } from 'lodash';
|
||||
import { isArray, isEmpty, isString, isUndefined } from 'lodash';
|
||||
import React, { FC, Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as IconEdit } from '../../../assets/svg/edit-new.svg';
|
||||
@ -66,16 +66,21 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
const handlePropertyUpdate = async (data: FormData) => {
|
||||
const updatedProperties = customProperties.map((property) => {
|
||||
if (property.name === selectedProperty.name) {
|
||||
const config = data.customPropertyConfig;
|
||||
const isEnumType = selectedProperty.propertyType.name === 'enum';
|
||||
|
||||
return {
|
||||
...property,
|
||||
description: data.description,
|
||||
...(data.customPropertyConfig
|
||||
...(config
|
||||
? {
|
||||
customPropertyConfig: {
|
||||
config: {
|
||||
multiSelect: Boolean(data?.multiSelect),
|
||||
values: data.customPropertyConfig,
|
||||
},
|
||||
config: isEnumType
|
||||
? {
|
||||
multiSelect: Boolean(data?.multiSelect),
|
||||
values: config,
|
||||
}
|
||||
: config,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@ -116,14 +121,24 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
title: t('label.config'),
|
||||
dataIndex: 'customPropertyConfig',
|
||||
key: 'customPropertyConfig',
|
||||
render: (data: CustomProperty['customPropertyConfig']) => {
|
||||
render: (data: CustomProperty['customPropertyConfig'], record) => {
|
||||
if (isUndefined(data)) {
|
||||
return <span>--</span>;
|
||||
}
|
||||
|
||||
const config = data.config;
|
||||
|
||||
if (!isString(config)) {
|
||||
// If config is an array and not empty
|
||||
if (isArray(config) && !isEmpty(config)) {
|
||||
return (
|
||||
<Typography.Text data-testid={`${record.name}-config`}>
|
||||
{JSON.stringify(config ?? [])}
|
||||
</Typography.Text>
|
||||
);
|
||||
}
|
||||
|
||||
// If config is an object, then it is a enum config
|
||||
if (!isString(config) && !isArray(config)) {
|
||||
return (
|
||||
<Space data-testid="enum-config" direction="vertical" size={4}>
|
||||
<Typography.Text>
|
||||
@ -137,6 +152,7 @@ export const CustomPropertyTable: FC<CustomPropertyTableProp> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// else it is a string
|
||||
return <Typography.Text>{config}</Typography.Text>;
|
||||
},
|
||||
},
|
||||
|
||||
@ -14,6 +14,10 @@ import { Form, Modal, Typography } from 'antd';
|
||||
import { isUndefined, uniq } from 'lodash';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ENTITY_REFERENCE_OPTIONS,
|
||||
PROPERTY_TYPES_WITH_ENTITY_REFERENCE,
|
||||
} from '../../../../constants/CustomProperty.constants';
|
||||
import {
|
||||
CustomProperty,
|
||||
EnumConfig,
|
||||
@ -54,6 +58,18 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const { hasEnumConfig, hasEntityReferenceConfig } = useMemo(() => {
|
||||
const propertyName = customProperty.propertyType.name ?? '';
|
||||
const hasEnumConfig = propertyName === 'enum';
|
||||
const hasEntityReferenceConfig =
|
||||
PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(propertyName);
|
||||
|
||||
return {
|
||||
hasEnumConfig,
|
||||
hasEntityReferenceConfig,
|
||||
};
|
||||
}, [customProperty]);
|
||||
|
||||
const formFields: FieldProp[] = [
|
||||
{
|
||||
name: 'description',
|
||||
@ -68,7 +84,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
},
|
||||
];
|
||||
|
||||
const customPropertyConfigField: FieldProp = {
|
||||
const enumConfigField: FieldProp = {
|
||||
name: 'customPropertyConfig',
|
||||
required: false,
|
||||
label: t('label.enum-value-plural'),
|
||||
@ -95,6 +111,37 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
],
|
||||
};
|
||||
|
||||
const entityReferenceConfigField: FieldProp = {
|
||||
name: 'customPropertyConfig',
|
||||
required: false,
|
||||
label: t('label.entity-reference-types'),
|
||||
id: 'root/customPropertyConfig',
|
||||
type: FieldTypes.SELECT,
|
||||
props: {
|
||||
'data-testid': 'customPropertyConfig',
|
||||
mode: 'multiple',
|
||||
options: ENTITY_REFERENCE_OPTIONS,
|
||||
placeholder: t('label.entity-reference-types'),
|
||||
onChange: (value: string[]) => {
|
||||
const entityReferenceConfig = customProperty.customPropertyConfig
|
||||
?.config as string[];
|
||||
const updatedValues = uniq([
|
||||
...value,
|
||||
...(entityReferenceConfig ?? []),
|
||||
]);
|
||||
form.setFieldsValue({ customPropertyConfig: updatedValues });
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.entity-reference-types'),
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const multiSelectField: FieldProp = {
|
||||
name: 'multiSelect',
|
||||
label: t('label.multi-select'),
|
||||
@ -108,8 +155,7 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
};
|
||||
|
||||
const initialValues = useMemo(() => {
|
||||
const isEnumType = customProperty.propertyType.name === 'enum';
|
||||
if (isEnumType) {
|
||||
if (hasEnumConfig) {
|
||||
const enumConfig = customProperty.customPropertyConfig
|
||||
?.config as EnumConfig;
|
||||
|
||||
@ -124,7 +170,17 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
description: customProperty.description,
|
||||
customPropertyConfig: customProperty.customPropertyConfig?.config,
|
||||
};
|
||||
}, [customProperty]);
|
||||
}, [customProperty, hasEnumConfig]);
|
||||
|
||||
const note = (
|
||||
<Typography.Text
|
||||
className="text-grey-muted"
|
||||
style={{ display: 'block', marginTop: '-18px' }}>
|
||||
{`Note: ${t(
|
||||
'message.updating-existing-not-possible-can-add-new-values'
|
||||
)}`}
|
||||
</Typography.Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -160,15 +216,20 @@ const EditCustomPropertyModal: FC<EditCustomPropertyModalProps> = ({
|
||||
{generateFormFields(formFields)}
|
||||
{!isUndefined(customProperty.customPropertyConfig) && (
|
||||
<>
|
||||
{generateFormFields([customPropertyConfigField])}
|
||||
<Typography.Text
|
||||
className="text-grey-muted"
|
||||
style={{ display: 'block', marginTop: '-18px' }}>
|
||||
{`Note: ${t(
|
||||
'message.updating-existing-not-possible-can-add-new-values'
|
||||
)}`}
|
||||
</Typography.Text>
|
||||
{generateFormFields([multiSelectField])}
|
||||
{hasEnumConfig && (
|
||||
<>
|
||||
{generateFormFields([enumConfigField])}
|
||||
{note}
|
||||
{generateFormFields([multiSelectField])}
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasEntityReferenceConfig && (
|
||||
<>
|
||||
{generateFormFields([entityReferenceConfigField])}
|
||||
{note}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
|
||||
@ -23,6 +23,8 @@ import { SearchIndex } from '../../../generated/entity/data/searchIndex';
|
||||
import { StoredProcedure } from '../../../generated/entity/data/storedProcedure';
|
||||
import { Table } from '../../../generated/entity/data/table';
|
||||
import { Topic } from '../../../generated/entity/data/topic';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
import { CustomProperty } from '../../../generated/type/customProperty';
|
||||
|
||||
export type ExtentionEntities = {
|
||||
[EntityType.TABLE]: Table;
|
||||
@ -51,3 +53,25 @@ export interface CustomPropertyProps<T extends ExtentionEntitiesKeys> {
|
||||
maxDataCap?: number;
|
||||
isRenderedInRightPanel?: boolean;
|
||||
}
|
||||
|
||||
export interface PropertyValueProps {
|
||||
property: CustomProperty;
|
||||
extension: Table['extension'];
|
||||
hasEditPermissions: boolean;
|
||||
versionDataKeys?: string[];
|
||||
isVersionView?: boolean;
|
||||
onExtensionUpdate: (updatedExtension: Table['extension']) => Promise<void>;
|
||||
}
|
||||
|
||||
export type TimeIntervalType = {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
export type PropertyValueType =
|
||||
| string
|
||||
| number
|
||||
| string[]
|
||||
| EntityReference
|
||||
| EntityReference[]
|
||||
| TimeIntervalType;
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
*/
|
||||
import { Table, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { isString, map } from 'lodash';
|
||||
import { isObject, isString, map } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';
|
||||
@ -58,7 +58,7 @@ export const ExtensionTable = ({
|
||||
|
||||
return (
|
||||
<Typography.Text className="break-all" data-testid="value">
|
||||
{value}
|
||||
{isObject(value) ? JSON.stringify(value ?? '{}', null, 2) : value}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { PropertyValue } from './PropertyValue';
|
||||
|
||||
jest.mock('../../common/RichTextEditor/RichTextEditorPreviewer', () => {
|
||||
@ -40,6 +41,21 @@ jest.mock('./PropertyInput', () => ({
|
||||
)),
|
||||
}));
|
||||
|
||||
jest.mock('../../Database/SchemaEditor/SchemaEditor', () =>
|
||||
jest.fn().mockReturnValue(<div data-testid="SchemaEditor">SchemaEditor</div>)
|
||||
);
|
||||
jest.mock(
|
||||
'../../DataAssets/DataAssetAsyncSelectList/DataAssetAsyncSelectList',
|
||||
() =>
|
||||
jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
<div data-testid="entity-reference-select">
|
||||
DataAssetAsyncSelectList
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
const mockUpdate = jest.fn();
|
||||
|
||||
const mockData = {
|
||||
@ -114,7 +130,7 @@ describe('Test PropertyValue Component', () => {
|
||||
});
|
||||
|
||||
it('Should render select component for enum type', async () => {
|
||||
const extension = { yNumber: 'enumValue' };
|
||||
const extension = { yNumber: ['enumValue1', 'enumValue2'] };
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'enum',
|
||||
@ -129,10 +145,329 @@ describe('Test PropertyValue Component', () => {
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(await screen.findByTestId('enum-value')).toHaveTextContent(
|
||||
'enumValue1, enumValue2'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('enum-select')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render date picker component for "date" type', async () => {
|
||||
const extension = { yNumber: '20-03-2024' };
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'date',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(await screen.findByTestId('value')).toHaveTextContent('20-03-2024');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('date-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render date picker component for "dateTime" type', async () => {
|
||||
const extension = {
|
||||
yNumber: '20-03-2024 2:00:00',
|
||||
};
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'dateTime',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(await screen.findByTestId('value')).toHaveTextContent(
|
||||
'20-03-2024 2:00:00'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('date-time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render time picker component for "time" type', async () => {
|
||||
const extension = {
|
||||
yNumber: '2:00:00',
|
||||
};
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'time',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(await screen.findByTestId('value')).toHaveTextContent('2:00:00');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('time-picker')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render email input component for "email" type', async () => {
|
||||
const extension = {
|
||||
yNumber: 'john@doe.com',
|
||||
};
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'email',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(await screen.findByTestId('value')).toHaveTextContent(
|
||||
'john@doe.com'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('email-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render timestamp input component for "timestamp" type', async () => {
|
||||
const extension = {
|
||||
yNumber: 1736255200000,
|
||||
};
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'timestamp',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(await screen.findByTestId('value')).toHaveTextContent(
|
||||
'1736255200000'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('timestamp-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render start and end input component for "timeInterval" type', async () => {
|
||||
const extension = {
|
||||
yNumber: {
|
||||
start: '1736255200000',
|
||||
end: '1736255200020',
|
||||
},
|
||||
};
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'timeInterval',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(await screen.findByTestId('time-interval-value')).toHaveTextContent(
|
||||
'StartTime: 1736255200000EndTime: 1736255200020'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('start-input')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('end-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render duration input component for "duration" type', async () => {
|
||||
const extension = {
|
||||
yNumber: 'P1Y2M3DT4H5M6S',
|
||||
};
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'duration',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(await screen.findByTestId('value')).toHaveTextContent(
|
||||
'P1Y2M3DT4H5M6S'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('duration-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render sqlQuery editor component for "sqlQuery" type', async () => {
|
||||
const extension = {};
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'sqlQuery',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('enum-select')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('SchemaEditor')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render entity reference select component for "entityReference" type', async () => {
|
||||
const extension = {
|
||||
yNumber: {
|
||||
id: 'entityReferenceId',
|
||||
name: 'entityReferenceName',
|
||||
fullyQualifiedName: 'entityReferenceFullyQualifiedName',
|
||||
type: 'entityReference',
|
||||
},
|
||||
};
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'entityReference',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>,
|
||||
{ wrapper: MemoryRouter }
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('entityReference-value')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('entityReference-value-name')
|
||||
).toHaveTextContent('entityReferenceName');
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('entity-reference-select')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render entity reference select component for "entityReferenceList" type', async () => {
|
||||
const extension = {
|
||||
yNumber: [
|
||||
{
|
||||
id: 'entityReferenceId',
|
||||
name: 'entityReferenceName',
|
||||
fullyQualifiedName: 'entityReferenceFullyQualifiedName',
|
||||
type: 'entityReference',
|
||||
},
|
||||
{
|
||||
id: 'entityReferenceId2',
|
||||
name: 'entityReferenceName2',
|
||||
fullyQualifiedName: 'entityReferenceFullyQualifiedName2',
|
||||
type: 'entityReference',
|
||||
},
|
||||
],
|
||||
};
|
||||
const propertyType = {
|
||||
...mockData.property.propertyType,
|
||||
name: 'entityReferenceList',
|
||||
};
|
||||
render(
|
||||
<PropertyValue
|
||||
{...mockData}
|
||||
extension={extension}
|
||||
property={{ ...mockData.property, propertyType: propertyType }}
|
||||
/>,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
|
||||
const iconElement = await screen.findByTestId('edit-icon');
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('entityReferenceName')
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('entityReferenceName2')
|
||||
).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(iconElement);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('entity-reference-select')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,34 +12,53 @@
|
||||
*/
|
||||
|
||||
import Icon from '@ant-design/icons';
|
||||
import { Form, Select, Tooltip, Typography } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
TimePicker,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { t } from 'i18next';
|
||||
import { isArray, isEmpty, isUndefined, noop, toNumber } from 'lodash';
|
||||
import React, { FC, Fragment, useState } from 'react';
|
||||
import { isArray, isUndefined, noop, toNumber, toUpper } from 'lodash';
|
||||
import moment, { Moment } from 'moment';
|
||||
import React, { CSSProperties, FC, Fragment, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ReactComponent as EditIconComponent } from '../../../assets/svg/edit-new.svg';
|
||||
import { DE_ACTIVE_COLOR, ICON_DIMENSION } from '../../../constants/constants';
|
||||
import { Table } from '../../../generated/entity/data/table';
|
||||
import {
|
||||
CustomProperty,
|
||||
EnumConfig,
|
||||
} from '../../../generated/type/customProperty';
|
||||
DE_ACTIVE_COLOR,
|
||||
ICON_DIMENSION,
|
||||
VALIDATION_MESSAGES,
|
||||
} from '../../../constants/constants';
|
||||
import { TIMESTAMP_UNIX_IN_MILLISECONDS_REGEX } from '../../../constants/regex.constants';
|
||||
import { CSMode } from '../../../enums/codemirror.enum';
|
||||
import { SearchIndex } from '../../../enums/search.enum';
|
||||
import { EntityReference } from '../../../generated/entity/type';
|
||||
import { EnumConfig } from '../../../generated/type/customProperty';
|
||||
import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { getEntityIcon } from '../../../utils/TableUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import DataAssetAsyncSelectList from '../../DataAssets/DataAssetAsyncSelectList/DataAssetAsyncSelectList';
|
||||
import { DataAssetOption } from '../../DataAssets/DataAssetAsyncSelectList/DataAssetAsyncSelectList.interface';
|
||||
import SchemaEditor from '../../Database/SchemaEditor/SchemaEditor';
|
||||
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||
import InlineEdit from '../InlineEdit/InlineEdit.component';
|
||||
import ProfilePicture from '../ProfilePicture/ProfilePicture';
|
||||
import RichTextEditorPreviewer from '../RichTextEditor/RichTextEditorPreviewer';
|
||||
import {
|
||||
PropertyValueProps,
|
||||
PropertyValueType,
|
||||
TimeIntervalType,
|
||||
} from './CustomPropertyTable.interface';
|
||||
import './property-value.less';
|
||||
import { PropertyInput } from './PropertyInput';
|
||||
|
||||
interface Props {
|
||||
versionDataKeys?: string[];
|
||||
isVersionView?: boolean;
|
||||
property: CustomProperty;
|
||||
extension: Table['extension'];
|
||||
onExtensionUpdate: (updatedExtension: Table['extension']) => Promise<void>;
|
||||
hasEditPermissions: boolean;
|
||||
}
|
||||
|
||||
export const PropertyValue: FC<Props> = ({
|
||||
export const PropertyValue: FC<PropertyValueProps> = ({
|
||||
isVersionView,
|
||||
versionDataKeys,
|
||||
extension,
|
||||
@ -55,28 +74,29 @@ export const PropertyValue: FC<Props> = ({
|
||||
const [showInput, setShowInput] = useState<boolean>(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const onShowInput = () => {
|
||||
setShowInput(true);
|
||||
};
|
||||
const onShowInput = () => setShowInput(true);
|
||||
|
||||
const onHideInput = () => {
|
||||
setShowInput(false);
|
||||
};
|
||||
const onHideInput = () => setShowInput(false);
|
||||
|
||||
const onInputSave = async (updatedValue: string | number | string[]) => {
|
||||
const onInputSave = async (updatedValue: PropertyValueType) => {
|
||||
const isEnum = propertyType.name === 'enum';
|
||||
|
||||
const isArrayType = isArray(updatedValue);
|
||||
|
||||
const enumValue = isArrayType ? updatedValue : [updatedValue];
|
||||
|
||||
const propertyValue = isEnum ? enumValue : updatedValue;
|
||||
|
||||
try {
|
||||
const updatedExtension = {
|
||||
...(extension || {}),
|
||||
[propertyName]:
|
||||
propertyType.name === 'integer'
|
||||
? toNumber(updatedValue || 0)
|
||||
: propertyValue,
|
||||
[propertyName]: ['integer', 'number'].includes(propertyType.name ?? '')
|
||||
? toNumber(updatedValue || 0)
|
||||
: propertyValue,
|
||||
};
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await onExtensionUpdate(updatedExtension);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
@ -87,26 +107,39 @@ export const PropertyValue: FC<Props> = ({
|
||||
};
|
||||
|
||||
const getPropertyInput = () => {
|
||||
const commonStyle: CSSProperties = {
|
||||
marginBottom: '0px',
|
||||
minWidth: '250px',
|
||||
};
|
||||
switch (propertyType.name) {
|
||||
case 'string':
|
||||
case 'integer':
|
||||
case 'number': {
|
||||
const inputType = ['integer', 'number'].includes(propertyType.name)
|
||||
? 'number'
|
||||
: 'text';
|
||||
|
||||
return (
|
||||
<PropertyInput
|
||||
isLoading={isLoading}
|
||||
propertyName={propertyName}
|
||||
type={propertyType.name === 'integer' ? 'number' : 'text'}
|
||||
type={inputType}
|
||||
value={value}
|
||||
onCancel={onHideInput}
|
||||
onSave={onInputSave}
|
||||
/>
|
||||
);
|
||||
case 'markdown':
|
||||
}
|
||||
|
||||
case 'markdown': {
|
||||
const header = t('label.edit-entity-name', {
|
||||
entityType: t('label.property'),
|
||||
entityName: propertyName,
|
||||
});
|
||||
|
||||
return (
|
||||
<ModalWithMarkdownEditor
|
||||
header={t('label.edit-entity-name', {
|
||||
entityType: t('label.property'),
|
||||
entityName: propertyName,
|
||||
})}
|
||||
header={header}
|
||||
placeholder={t('label.enter-property-value')}
|
||||
value={value || ''}
|
||||
visible={showInput}
|
||||
@ -114,14 +147,22 @@ export const PropertyValue: FC<Props> = ({
|
||||
onSave={onInputSave}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'enum': {
|
||||
const enumConfig = property.customPropertyConfig?.config as EnumConfig;
|
||||
|
||||
const isMultiSelect = Boolean(enumConfig?.multiSelect);
|
||||
|
||||
const options = enumConfig?.values?.map((option) => ({
|
||||
label: option,
|
||||
value: option,
|
||||
}));
|
||||
|
||||
const initialValues = {
|
||||
enumValues: (isArray(value) ? value : [value]).filter(Boolean),
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
@ -134,9 +175,7 @@ export const PropertyValue: FC<Props> = ({
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="enum-form"
|
||||
initialValues={{
|
||||
enumValues: (isArray(value) ? value : [value]).filter(Boolean),
|
||||
}}
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
onFinish={(values: { enumValues: string | string[] }) =>
|
||||
onInputSave(values.enumValues)
|
||||
@ -151,13 +190,61 @@ export const PropertyValue: FC<Props> = ({
|
||||
}),
|
||||
},
|
||||
]}
|
||||
style={{ marginBottom: '0px' }}>
|
||||
style={commonStyle}>
|
||||
<Select
|
||||
data-testid="enum-select"
|
||||
disabled={isLoading}
|
||||
mode={isMultiSelect ? 'multiple' : undefined}
|
||||
options={options}
|
||||
placeholder={t('label.enum-value-plural')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
case 'date':
|
||||
case 'dateTime': {
|
||||
const format = toUpper(property.customPropertyConfig?.config as string);
|
||||
|
||||
const initialValues = {
|
||||
dateTimeValue: value ? moment(value, format) : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'dateTime-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="dateTime-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
onFinish={(values: { dateTimeValue: Moment }) => {
|
||||
onInputSave(values.dateTimeValue.format(format));
|
||||
}}>
|
||||
<Form.Item
|
||||
name="dateTimeValue"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: propertyType.name,
|
||||
}),
|
||||
},
|
||||
]}
|
||||
style={commonStyle}>
|
||||
<DatePicker
|
||||
data-testid="date-time-picker"
|
||||
disabled={isLoading}
|
||||
format={format}
|
||||
showTime={propertyType.name === 'dateTime'}
|
||||
style={{ width: '250px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
@ -166,6 +253,385 @@ export const PropertyValue: FC<Props> = ({
|
||||
);
|
||||
}
|
||||
|
||||
case 'time': {
|
||||
const format = 'HH:mm:ss';
|
||||
const initialValues = {
|
||||
time: value ? moment(value, format) : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'time-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="time-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
validateMessages={VALIDATION_MESSAGES}
|
||||
onFinish={(values: { time: Moment }) => {
|
||||
onInputSave(values.time.format(format));
|
||||
}}>
|
||||
<Form.Item
|
||||
name="time"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
style={commonStyle}>
|
||||
<TimePicker
|
||||
data-testid="time-picker"
|
||||
disabled={isLoading}
|
||||
style={{ width: '250px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
case 'email': {
|
||||
const initialValues = {
|
||||
email: value,
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'email-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="email-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
validateMessages={VALIDATION_MESSAGES}
|
||||
onFinish={(values: { email: string }) => {
|
||||
onInputSave(values.email);
|
||||
}}>
|
||||
<Form.Item
|
||||
name="email"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
min: 6,
|
||||
max: 127,
|
||||
type: 'email',
|
||||
},
|
||||
]}
|
||||
style={commonStyle}>
|
||||
<Input
|
||||
data-testid="email-input"
|
||||
disabled={isLoading}
|
||||
placeholder="john@doe.com"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
case 'timestamp': {
|
||||
const initialValues = {
|
||||
timestamp: value,
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'timestamp-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="timestamp-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
onFinish={(values: { timestamp: string }) => {
|
||||
onInputSave(toNumber(values.timestamp));
|
||||
}}>
|
||||
<Form.Item
|
||||
name="timestamp"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: TIMESTAMP_UNIX_IN_MILLISECONDS_REGEX,
|
||||
},
|
||||
]}
|
||||
style={commonStyle}>
|
||||
<Input
|
||||
data-testid="timestamp-input"
|
||||
disabled={isLoading}
|
||||
placeholder={t('message.unix-epoch-time-in-ms', {
|
||||
prefix: '',
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
case 'timeInterval': {
|
||||
const initialValues = {
|
||||
start: value?.start ? value.start?.toString() : undefined,
|
||||
end: value?.end ? value.end?.toString() : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'timeInterval-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="timeInterval-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
onFinish={(values: { start: string; end: string }) => {
|
||||
onInputSave({
|
||||
start: toNumber(values.start),
|
||||
end: toNumber(values.end),
|
||||
});
|
||||
}}>
|
||||
<Form.Item
|
||||
name="start"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: TIMESTAMP_UNIX_IN_MILLISECONDS_REGEX,
|
||||
},
|
||||
]}
|
||||
style={{ ...commonStyle, marginBottom: '16px' }}>
|
||||
<Input
|
||||
data-testid="start-input"
|
||||
disabled={isLoading}
|
||||
placeholder={t('message.unix-epoch-time-in-ms', {
|
||||
prefix: 'Start',
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="end"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
pattern: TIMESTAMP_UNIX_IN_MILLISECONDS_REGEX,
|
||||
},
|
||||
]}
|
||||
style={commonStyle}>
|
||||
<Input
|
||||
data-testid="end-input"
|
||||
disabled={isLoading}
|
||||
placeholder={t('message.unix-epoch-time-in-ms', {
|
||||
prefix: 'End',
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
case 'duration': {
|
||||
const initialValues = {
|
||||
duration: value,
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'duration-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="duration-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
validateMessages={VALIDATION_MESSAGES}
|
||||
onFinish={(values: { duration: string }) => {
|
||||
onInputSave(values.duration);
|
||||
}}>
|
||||
<Form.Item
|
||||
name="duration"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
style={commonStyle}>
|
||||
<Input
|
||||
data-testid="duration-input"
|
||||
disabled={isLoading}
|
||||
placeholder={t('message.duration-in-iso-format')}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
case 'sqlQuery': {
|
||||
const initialValues = {
|
||||
sqlQuery: value,
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'sqlQuery-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="sqlQuery-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
validateMessages={VALIDATION_MESSAGES}
|
||||
onFinish={(values: { sqlQuery: string }) => {
|
||||
onInputSave(values.sqlQuery);
|
||||
}}>
|
||||
<Form.Item
|
||||
name="sqlQuery"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: t('label.field-required', {
|
||||
field: t('label.sql-uppercase-query'),
|
||||
}),
|
||||
},
|
||||
]}
|
||||
style={commonStyle}
|
||||
trigger="onChange">
|
||||
<SchemaEditor
|
||||
className="custom-query-editor query-editor-h-200 custom-code-mirror-theme"
|
||||
mode={{ name: CSMode.SQL }}
|
||||
options={{
|
||||
readOnly: false,
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
case 'entityReference':
|
||||
case 'entityReferenceList': {
|
||||
const mode =
|
||||
propertyType.name === 'entityReferenceList' ? 'multiple' : undefined;
|
||||
|
||||
const index = (property.customPropertyConfig?.config as string[]) ?? [];
|
||||
|
||||
let initialOptions: DataAssetOption[] = [];
|
||||
let initialValue: string[] | string | undefined;
|
||||
|
||||
if (!isUndefined(value)) {
|
||||
if (isArray(value)) {
|
||||
initialOptions = value.map((item: EntityReference) => {
|
||||
return {
|
||||
displayName: getEntityName(item),
|
||||
reference: item,
|
||||
label: getEntityName(item),
|
||||
value: item?.fullyQualifiedName ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
initialValue = value.map(
|
||||
(item: EntityReference) => item?.fullyQualifiedName ?? ''
|
||||
);
|
||||
} else {
|
||||
initialOptions = [
|
||||
{
|
||||
displayName: getEntityName(value),
|
||||
reference: value,
|
||||
label: getEntityName(value),
|
||||
value: value?.fullyQualifiedName ?? '',
|
||||
},
|
||||
];
|
||||
|
||||
initialValue = value?.fullyQualifiedName ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
entityReference: initialValue,
|
||||
};
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
isLoading={isLoading}
|
||||
saveButtonProps={{
|
||||
disabled: isLoading,
|
||||
htmlType: 'submit',
|
||||
form: 'entity-reference-form',
|
||||
}}
|
||||
onCancel={onHideInput}
|
||||
onSave={noop}>
|
||||
<Form
|
||||
id="entity-reference-form"
|
||||
initialValues={initialValues}
|
||||
layout="vertical"
|
||||
validateMessages={VALIDATION_MESSAGES}
|
||||
onFinish={(values: {
|
||||
entityReference: DataAssetOption | DataAssetOption[];
|
||||
}) => {
|
||||
if (isArray(values.entityReference)) {
|
||||
onInputSave(
|
||||
values.entityReference.map((item) => item.reference)
|
||||
);
|
||||
} else {
|
||||
onInputSave(values.entityReference.reference);
|
||||
}
|
||||
}}>
|
||||
<Form.Item
|
||||
name="entityReference"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
style={commonStyle}>
|
||||
<DataAssetAsyncSelectList
|
||||
initialOptions={initialOptions}
|
||||
mode={mode}
|
||||
placeholder={
|
||||
mode === 'multiple'
|
||||
? t('label.entity-reference')
|
||||
: t('label.entity-reference-plural')
|
||||
}
|
||||
searchIndex={index.join(',') as SearchIndex}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</InlineEdit>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -188,13 +654,144 @@ export const PropertyValue: FC<Props> = ({
|
||||
|
||||
case 'enum':
|
||||
return (
|
||||
<Typography.Text className="break-all" data-testid="value">
|
||||
<Typography.Text className="break-all" data-testid="enum-value">
|
||||
{isArray(value) ? value.join(', ') : value}
|
||||
</Typography.Text>
|
||||
);
|
||||
|
||||
case 'sqlQuery':
|
||||
return (
|
||||
<SchemaEditor
|
||||
className="custom-query-editor query-editor-h-200 custom-code-mirror-theme"
|
||||
mode={{ name: CSMode.SQL }}
|
||||
options={{
|
||||
readOnly: true,
|
||||
}}
|
||||
value={value ?? ''}
|
||||
/>
|
||||
);
|
||||
case 'entityReferenceList': {
|
||||
const entityReferences = (value as EntityReference[]) ?? [];
|
||||
|
||||
return (
|
||||
<div className="entity-list-body">
|
||||
{entityReferences.map((item) => {
|
||||
return (
|
||||
<div
|
||||
className="entity-reference-list-item flex items-center justify-between"
|
||||
data-testid={getEntityName(item)}
|
||||
key={item.id}>
|
||||
<div className="d-flex items-center">
|
||||
<Link
|
||||
to={entityUtilClassBase.getEntityLink(
|
||||
item.type,
|
||||
item.fullyQualifiedName as string
|
||||
)}>
|
||||
<Button
|
||||
className="entity-button flex-center p-0 m--ml-1"
|
||||
icon={
|
||||
<div className="entity-button-icon m-r-xs">
|
||||
{['user', 'team'].includes(item.type) ? (
|
||||
<ProfilePicture
|
||||
className="d-flex"
|
||||
isTeam={item.type === 'team'}
|
||||
name={item.name ?? ''}
|
||||
type="circle"
|
||||
width="18"
|
||||
/>
|
||||
) : (
|
||||
getEntityIcon(item.type)
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
type="text">
|
||||
<Typography.Text
|
||||
className="text-left text-xs"
|
||||
ellipsis={{ tooltip: true }}>
|
||||
{getEntityName(item)}
|
||||
</Typography.Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'entityReference': {
|
||||
const item = value as EntityReference;
|
||||
|
||||
if (isUndefined(item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="d-flex items-center"
|
||||
data-testid="entityReference-value">
|
||||
<Link
|
||||
to={entityUtilClassBase.getEntityLink(
|
||||
item.type,
|
||||
item.fullyQualifiedName as string
|
||||
)}>
|
||||
<Button
|
||||
className="entity-button flex-center p-0 m--ml-1"
|
||||
icon={
|
||||
<div className="entity-button-icon m-r-xs">
|
||||
{['user', 'team'].includes(item.type) ? (
|
||||
<ProfilePicture
|
||||
className="d-flex"
|
||||
isTeam={item.type === 'team'}
|
||||
name={item.name ?? ''}
|
||||
type="circle"
|
||||
width="18"
|
||||
/>
|
||||
) : (
|
||||
getEntityIcon(item.type)
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
type="text">
|
||||
<Typography.Text
|
||||
className="text-left text-xs"
|
||||
data-testid="entityReference-value-name"
|
||||
ellipsis={{ tooltip: true }}>
|
||||
{getEntityName(item)}
|
||||
</Typography.Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'timeInterval': {
|
||||
const timeInterval = value as TimeIntervalType;
|
||||
|
||||
if (isUndefined(timeInterval)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography.Text
|
||||
className="break-all"
|
||||
data-testid="time-interval-value">
|
||||
{`StartTime: ${timeInterval.start}`}
|
||||
<br />
|
||||
{`EndTime: ${timeInterval.end}`}
|
||||
</Typography.Text>
|
||||
);
|
||||
}
|
||||
|
||||
case 'string':
|
||||
case 'integer':
|
||||
case 'number':
|
||||
case 'date':
|
||||
case 'dateTime':
|
||||
case 'time':
|
||||
case 'email':
|
||||
case 'timestamp':
|
||||
case 'duration':
|
||||
default:
|
||||
return (
|
||||
<Typography.Text className="break-all" data-testid="value">
|
||||
@ -206,24 +803,14 @@ export const PropertyValue: FC<Props> = ({
|
||||
|
||||
const getValueElement = () => {
|
||||
const propertyValue = getPropertyValue();
|
||||
const isInteger = propertyType.name === 'integer';
|
||||
if (isInteger) {
|
||||
return !isUndefined(value) ? (
|
||||
propertyValue
|
||||
) : (
|
||||
<span className="text-grey-muted" data-testid="no-data">
|
||||
{t('message.no-data')}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return !isEmpty(value) ? (
|
||||
propertyValue
|
||||
) : (
|
||||
<span className="text-grey-muted" data-testid="no-data">
|
||||
{t('message.no-data')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return !isUndefined(value) ? (
|
||||
propertyValue
|
||||
) : (
|
||||
<span className="text-grey-muted" data-testid="no-data">
|
||||
{t('message.no-data')}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
.entity-reference-list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.entity-reference-list-item {
|
||||
.entity-button-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2024 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export const PROPERTY_TYPES_WITH_FORMAT = ['date', 'dateTime'];
|
||||
|
||||
export const PROPERTY_TYPES_WITH_ENTITY_REFERENCE = [
|
||||
'entityReference',
|
||||
'entityReferenceList',
|
||||
];
|
||||
|
||||
export const ENTITY_REFERENCE_OPTIONS = [
|
||||
{
|
||||
key: 'table',
|
||||
value: 'table',
|
||||
label: 'Table',
|
||||
},
|
||||
{
|
||||
key: 'storedProcedure',
|
||||
value: 'storedProcedure',
|
||||
label: 'Stored Procedure',
|
||||
},
|
||||
{
|
||||
key: 'databaseSchema',
|
||||
value: 'databaseSchema',
|
||||
label: 'Database Schema',
|
||||
},
|
||||
{
|
||||
key: 'database',
|
||||
value: 'database',
|
||||
label: 'Database',
|
||||
},
|
||||
{
|
||||
key: 'dashboard',
|
||||
value: 'dashboard',
|
||||
label: 'Dashboard',
|
||||
},
|
||||
{
|
||||
key: 'dashboardDataModel',
|
||||
value: 'dashboardDataModel',
|
||||
label: 'Dashboard DataModel',
|
||||
},
|
||||
{
|
||||
key: 'pipeline',
|
||||
value: 'pipeline',
|
||||
label: 'Pipeline',
|
||||
},
|
||||
{
|
||||
key: 'topic',
|
||||
value: 'topic',
|
||||
label: 'Topic',
|
||||
},
|
||||
{
|
||||
key: 'container',
|
||||
value: 'container',
|
||||
label: 'Container',
|
||||
},
|
||||
{
|
||||
key: 'searchIndex',
|
||||
value: 'searchIndex',
|
||||
label: 'Search Index',
|
||||
},
|
||||
{
|
||||
key: 'mlmodel',
|
||||
value: 'mlmodel',
|
||||
label: 'MLmodel',
|
||||
},
|
||||
{
|
||||
key: 'glossaryTerm',
|
||||
value: 'glossaryTerm',
|
||||
label: 'Glossary Term',
|
||||
},
|
||||
{
|
||||
key: 'tag',
|
||||
value: 'tag',
|
||||
label: 'Tag',
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
value: 'user',
|
||||
label: 'User',
|
||||
},
|
||||
{
|
||||
key: 'team',
|
||||
value: 'team',
|
||||
label: 'Team',
|
||||
},
|
||||
];
|
||||
@ -47,7 +47,6 @@ export const LIGHT_GREEN_COLOR = '#4CAF50';
|
||||
export const DEFAULT_CHART_OPACITY = 1;
|
||||
export const HOVER_CHART_OPACITY = 0.3;
|
||||
|
||||
export const SUPPORTED_FIELD_TYPES = ['string', 'markdown', 'integer', 'enum'];
|
||||
export const LOGGED_IN_USER_STORAGE_KEY = 'loggedInUsers';
|
||||
export const ACTIVE_DOMAIN_STORAGE_KEY = 'activeDomain';
|
||||
export const DEFAULT_DOMAIN_VALUE = 'All Domains';
|
||||
|
||||
@ -50,3 +50,5 @@ export const VALID_OBJECT_KEY_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/;
|
||||
export const HEX_COLOR_CODE_REGEX = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
||||
|
||||
export const TASK_SANITIZE_VALUE_REGEX = /^"|"$/g;
|
||||
|
||||
export const TIMESTAMP_UNIX_IN_MILLISECONDS_REGEX = /^\d{13}$/;
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "Entitäten",
|
||||
"entity-proportion": "{{entity}}-Anteil",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "{{entity}}-Dienst",
|
||||
"entity-type-plural": "{{entity}}-Typen",
|
||||
"entity-version-detail-plural": "Details zu {{entity}}-Versionen",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "für",
|
||||
"foreign-key": "Fremdschlüssel",
|
||||
"forgot-password": "Passwort vergessen",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "FQN",
|
||||
"frequently-joined-column-plural": "Häufig verwendete Spalten in Verknüpfungen",
|
||||
"frequently-joined-table-plural": "Häufig verwendete Tabellen in Verknüpfungen",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "Zeigen Sie bis zu 3 Knoten der nachgelagerten Verbindungslinie an, um das Ziel (Kinderebenen) zu identifizieren.",
|
||||
"drag-and-drop-files-here": "Dateien hierher ziehen und ablegen",
|
||||
"drag-and-drop-or-browse-csv-files-here": "Ziehen und ablegen oder <0>{{text}}</0> CSV-Datei hier auswählen",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "Change icon and badge color for the {{entity}}.",
|
||||
"edit-glossary-display-name-help": "Display-Namen aktualisieren",
|
||||
"edit-glossary-name-help": "Die Änderung des Namens entfernt das vorhandene Tag und erstellt ein neues mit dem angegebenen Namen.",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "URL zum Verbinden mit Ihrer dbt Cloud-Instanz. Zum Beispiel \n https://cloud.getdbt.com oder https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "Wir können {{error}} Elasticsearch für Entität-Indizes nicht durchführen.",
|
||||
"uninstall-app": "Uninstalling this {{app}} application will remove it from OpenMetaData",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Request to update description for",
|
||||
"update-displayName-entity": "Aktualisieren Sie die Anzeigenamen für das {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "Entities",
|
||||
"entity-proportion": "{{entity}} Proportion",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "{{entity}} Service",
|
||||
"entity-type-plural": "{{entity}} Type",
|
||||
"entity-version-detail-plural": "{{entity}} Version Details",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "for",
|
||||
"foreign-key": "Foreign Key",
|
||||
"forgot-password": "Forgot Password",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "FQN",
|
||||
"frequently-joined-column-plural": "Frequently Joined Columns",
|
||||
"frequently-joined-table-plural": "Frequently Joined Tables",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
|
||||
"drag-and-drop-files-here": "Drag & drop files here",
|
||||
"drag-and-drop-or-browse-csv-files-here": "Drag & Drop or <0>{{text}}</0> CSV file here",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "Change icon and badge color for the {{entity}}.",
|
||||
"edit-glossary-display-name-help": "Update Display Name",
|
||||
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "URL to connect to your dbt cloud instance. E.g., \n https://cloud.getdbt.com or https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "We are unable to {{error}} Elasticsearch for entity indexes.",
|
||||
"uninstall-app": "Uninstalling this {{app}} application will remove it from OpenMetaData",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Request to update description for",
|
||||
"update-displayName-entity": "Update Display Name for the {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "Entidades",
|
||||
"entity-proportion": "Proporción de {{entity}}",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "Servicio de {{entity}}",
|
||||
"entity-type-plural": "Tipo de {{entity}}",
|
||||
"entity-version-detail-plural": "Detalles de versión de {{entity}}",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "para",
|
||||
"foreign-key": "Llave Foránea",
|
||||
"forgot-password": "Olvidé mi contraseña",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "FQN",
|
||||
"frequently-joined-column-plural": "Columnas frecuentemente unidas",
|
||||
"frequently-joined-table-plural": "Tablas frecuentemente unidas",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "Muestre hasta 3 nodos de linaje para identificar el objetivo (niveles descendientes).",
|
||||
"drag-and-drop-files-here": "Arrastre y suelte archivos aquí",
|
||||
"drag-and-drop-or-browse-csv-files-here": "Arrastre y suelte o <0>{{text}}</0> archivo CSV aquí",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "Cambie el icono y el color de la insignia para {{entity}}.",
|
||||
"edit-glossary-display-name-help": "Actualizar el nombre de visualización",
|
||||
"edit-glossary-name-help": "Cambiar el nombre eliminará la etiqueta existente y creará una nueva con el nombre mencionado.",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "URL para conectarse a su instancia de dbt cloud. Por ejemplo, \n https://cloud.getdbt.com o https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "No podemos {{error}} Elasticsearch para los índices de entidades.",
|
||||
"uninstall-app": "Desinstalar esta aplicación {{app}} la eliminará de OpenMetaData",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Solicitud para actualizar la descripción de",
|
||||
"update-displayName-entity": "Actualizar el nombre visualizado para el {{entity}}.",
|
||||
"update-profiler-settings": "Actualizar la configuración del perfilador.",
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "Entités",
|
||||
"entity-proportion": "Proportion de {{entity}}",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "Service de {{entity}}",
|
||||
"entity-type-plural": "{{entity}} Types",
|
||||
"entity-version-detail-plural": "Détails des Versions de {{entity}}",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "pour",
|
||||
"foreign-key": "Clé Étrangère",
|
||||
"forgot-password": "Mot de passe oublié",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "FQN",
|
||||
"frequently-joined-column-plural": "Colonnes fréquemment utilisées dans les jointures",
|
||||
"frequently-joined-table-plural": "Tables Fréquemment Utilisées dans les Jointures",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "Afficher jusqu'à 3 nœuds de lignée descendante pour identifier la cible (niveaux enfants).",
|
||||
"drag-and-drop-files-here": "Glisser et déposer les fichiers ici",
|
||||
"drag-and-drop-or-browse-csv-files-here": "Glisser et déposer ou <0>{{text}}</0> fichier CSV ici",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "Change icon and badge color for the {{entity}}.",
|
||||
"edit-glossary-display-name-help": "Mise à jour du nom d'affichage",
|
||||
"edit-glossary-name-help": "Changer le nom supprimera le tag existant et en créera un nouveau avec le nom mentionné",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "URL de connexion à votre instance dbt cloud. Par exemple, \n https://cloud.getdbt.com ou https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "Nous ne sommes pas en mesure de {{error}} Elasticsearch pour les index d'entités.",
|
||||
"uninstall-app": "Uninstalling this {{app}} application will remove it from OpenMetaData",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Request to update description for",
|
||||
"update-displayName-entity": "Mettre à Jour le Nom d'Affichage de {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "ישויות",
|
||||
"entity-proportion": "יחס {{entity}}",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "שירות {{entity}}",
|
||||
"entity-type-plural": "סוגי {{entity}}",
|
||||
"entity-version-detail-plural": "גרסאות פרטי {{entity}}",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "עבור",
|
||||
"foreign-key": "מפתח זר",
|
||||
"forgot-password": "שכחת סיסמה",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "FQN",
|
||||
"frequently-joined-column-plural": "עמודות הצטרפות תדירות",
|
||||
"frequently-joined-table-plural": "טבלאות הצטרפות תדירות",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "מציג עד ל-3 צמתים של לוקייניאג' מטה כדי לזהות את היעד (רמות הילדים).",
|
||||
"drag-and-drop-files-here": "גרור ושחרר קבצים לכאן",
|
||||
"drag-and-drop-or-browse-csv-files-here": "גרור ושחרר או <0>{{text}}</0> קובץ CSV לכאן",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "שנה את צבע הסמל והביסוס עבור {{entity}}.",
|
||||
"edit-glossary-display-name-help": "עדכן שם תצוגה",
|
||||
"edit-glossary-name-help": "שינוי שם יסיר את התג הקיים ויצור תג חדש בשם שצוין",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "כתובת ה-URL להתחברות למופע שלך של dbt Cloud. לדוגמה, https://cloud.getdbt.com או https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "אנו לא יכולים ל{{error}} לאלסטיקסרץ' שלך עבור אינדקסים של ישויות.",
|
||||
"uninstall-app": "הסרת התקנת {{app}} תסיר אותו מ-OpenMetadata",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Request to update description for",
|
||||
"update-displayName-entity": "עדכן את השם המוצג עבור {{entity}}.",
|
||||
"update-profiler-settings": "עדכן הגדרות הפרופיילר.",
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "エンティティ",
|
||||
"entity-proportion": "{{entity}} Proportion",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "{{entity}}サービス",
|
||||
"entity-type-plural": "{{entity}} Type",
|
||||
"entity-version-detail-plural": "{{entity}} Version Details",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "for",
|
||||
"foreign-key": "外部キー",
|
||||
"forgot-password": "パスワードを忘れた場合はこちら",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "FQN",
|
||||
"frequently-joined-column-plural": "よく結合されるカラム",
|
||||
"frequently-joined-table-plural": "よく結合されるテーブル",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "Display up to 3 nodes of downstream lineage to identify the target (child levels).",
|
||||
"drag-and-drop-files-here": "ここにファイルをドラッグ&ドロップ",
|
||||
"drag-and-drop-or-browse-csv-files-here": "Drag & Drop or <0>{{text}}</0> CSV file here",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "Change icon and badge color for the {{entity}}.",
|
||||
"edit-glossary-display-name-help": "Update Display Name",
|
||||
"edit-glossary-name-help": "Changing Name will remove the existing tag and create new one with mentioned name",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "URL to connect to your dbt cloud instance. E.g., \n https://cloud.getdbt.com or https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "We are unable to {{error}} Elasticsearch for entity indexes.",
|
||||
"uninstall-app": "Uninstalling this {{app}} application will remove it from OpenMetaData",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Request to update description for",
|
||||
"update-displayName-entity": "Update Display Name for the {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "Entiteiten",
|
||||
"entity-proportion": "{{entity}}-verhouding",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "{{entity}}-service",
|
||||
"entity-type-plural": "{{entity}}-type",
|
||||
"entity-version-detail-plural": "{{entity}}-versie-details",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "voor",
|
||||
"foreign-key": "Vreemde sleutel",
|
||||
"forgot-password": "Wachtwoord vergeten",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "VZV",
|
||||
"frequently-joined-column-plural": "Vaak gejoinde kolommen",
|
||||
"frequently-joined-table-plural": "Vaak gejoinde tabellen",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "Toon maximaal 3 knooppunten van downstream-lineage om het doel (kind-levels) te identificeren.",
|
||||
"drag-and-drop-files-here": "Bestanden hierheen slepen en neerzetten",
|
||||
"drag-and-drop-or-browse-csv-files-here": "Sleep & neerzetten, of <0>{{text}}</0> CSV-bestand hier",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "Wijzig het pictogram en de badgekleur voor de {{entity}}.",
|
||||
"edit-glossary-display-name-help": "Werk de weergavenaam bij",
|
||||
"edit-glossary-name-help": "Het wijzigen van de naam verwijdert het bestaande label en maakt een nieuw label met de vermelde naam aan",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "URL om connectie te maken met je dbt-cloudinstantie. Bijvoorbeeld, \n https://cloud.getdbt.com of https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "We kunnen geen connectie maken met Elasticsearch voor entiteitsindexen.",
|
||||
"uninstall-app": "Het verwijderen van deze {{app}}-toepassing verwijdert deze uit OpenMetaData",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Verzoek om de beschrijving aan te passen voor",
|
||||
"update-displayName-entity": "Update de weergavenaam voor de {{entity}}.",
|
||||
"update-profiler-settings": "Profielinstellingen updaten.",
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "Entidades",
|
||||
"entity-proportion": "Proporção de {{entity}}",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "Serviço de {{entity}}",
|
||||
"entity-type-plural": "Tipo de {{entity}}",
|
||||
"entity-version-detail-plural": "Detalhes da Versão de {{entity}}",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "para",
|
||||
"foreign-key": "Chave Estrangeira",
|
||||
"forgot-password": "Esqueceu a Senha",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "FQN",
|
||||
"frequently-joined-column-plural": "Colunas Frequentemente Unidas",
|
||||
"frequently-joined-table-plural": "Tabelas Frequentemente Unidas",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "Exibir até 3 nós de linhagem a jusante para identificar o alvo (níveis filhos).",
|
||||
"drag-and-drop-files-here": "Arraste e solte arquivos aqui",
|
||||
"drag-and-drop-or-browse-csv-files-here": "Arraste e Solte ou <0>{{text}}</0> arquivo CSV aqui",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "Mude o ícone e a cor do distintivo para a {{entity}}.",
|
||||
"edit-glossary-display-name-help": "Atualizar Nome de Exibição",
|
||||
"edit-glossary-name-help": "Mudar o Nome removerá a tag existente e criará uma nova com o nome mencionado",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "URL para se conectar à sua instância do dbt cloud. Exemplo: \n https://cloud.getdbt.com ou https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "Não é possível {{error}} Elasticsearch para índices de entidade.",
|
||||
"uninstall-app": "Desinstalar este aplicativo {{app}} removerá ele do OpenMetaData",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Solicitar atualização de descrição para",
|
||||
"update-displayName-entity": "Atualizar o Nome de Exibição para {{entity}}.",
|
||||
"update-profiler-settings": "Atualizar configurações do examinador.",
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "Сущности",
|
||||
"entity-proportion": "Доля \"{{entity}}\"",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "Сервис {{entity}}",
|
||||
"entity-type-plural": "Тип {{entity}}",
|
||||
"entity-version-detail-plural": "{{entity}} Version Details",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "для",
|
||||
"foreign-key": "Внешний ключ",
|
||||
"forgot-password": "Забыл пароль",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "FQN",
|
||||
"frequently-joined-column-plural": "Часто соединяемые столбцы",
|
||||
"frequently-joined-table-plural": "Часто соединяемые таблицы",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "Отобразите до 3 узлов нисходящей линии для идентификации цели (дочерние уровни).",
|
||||
"drag-and-drop-files-here": "Перетащите файлы сюда",
|
||||
"drag-and-drop-or-browse-csv-files-here": "Перетащите или <0>{{text}}</0> CSV-файл сюда ",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "Change icon and badge color for the {{entity}}.",
|
||||
"edit-glossary-display-name-help": "Обновить отображаемое имя",
|
||||
"edit-glossary-name-help": "Изменение имени удалит существующий тег и создаст новый с указанным именем.",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "URL для подключения к вашему экземпляру облака dbt. Например,\n https://cloud.getdbt.com или https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "Мы не можем выполнить {{error}} Elasticsearch для индексов сущностей.",
|
||||
"uninstall-app": "Uninstalling this {{app}} application will remove it from OpenMetaData",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Request to update description for",
|
||||
"update-displayName-entity": "Обновите отображаемое имя для {{entity}}.",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
|
||||
@ -413,6 +413,9 @@
|
||||
"entity-plural": "实体",
|
||||
"entity-proportion": "{{entity}}比例",
|
||||
"entity-record-plural": "{{entity}} Records",
|
||||
"entity-reference": "Entity Reference",
|
||||
"entity-reference-plural": "Entity References",
|
||||
"entity-reference-types": "Entity Reference Types",
|
||||
"entity-service": "{{entity}}服务",
|
||||
"entity-type-plural": "{{entity}}类型",
|
||||
"entity-version-detail-plural": "{{entity}}版本详情",
|
||||
@ -479,6 +482,7 @@
|
||||
"for-lowercase": "为了",
|
||||
"foreign-key": "外键",
|
||||
"forgot-password": "忘记密码",
|
||||
"format": "Format",
|
||||
"fqn-uppercase": "FQN",
|
||||
"frequently-joined-column-plural": "经常连接查询的列 (Frequently Joined Column)",
|
||||
"frequently-joined-table-plural": "经常连接查询的表 (Frequently Joined Table)",
|
||||
@ -1376,6 +1380,7 @@
|
||||
"downstream-depth-tooltip": "显示最多三个下游谱系节点以确定目标(子级)",
|
||||
"drag-and-drop-files-here": "拖放文件到此处",
|
||||
"drag-and-drop-or-browse-csv-files-here": "拖放或者<0>{{text}}</0> CSV 文件到此处",
|
||||
"duration-in-iso-format": "Duration in ISO 8601 format 'PnYnMnDTnHnMnS'",
|
||||
"edit-entity-style-description": "Change icon and badge color for the {{entity}}.",
|
||||
"edit-glossary-display-name-help": "更新显示名称",
|
||||
"edit-glossary-name-help": "修改名称将会删除现有的标签,并在新名称下新建标签",
|
||||
@ -1730,6 +1735,7 @@
|
||||
"unable-to-connect-to-your-dbt-cloud-instance": "连接到您的 dbt 云实例的 URL。例如,\n https://cloud.getdbt.com 或 https://emea.dbt.com/",
|
||||
"unable-to-error-elasticsearch": "无法为 Elasticsearch 进行实体索引{{error}}",
|
||||
"uninstall-app": "Uninstalling this {{app}} application will remove it from OpenMetaData",
|
||||
"unix-epoch-time-in-ms": "{{prefix}} Unix epoch time in milliseconds",
|
||||
"update-description-message": "Request to update description for",
|
||||
"update-displayName-entity": "更改{{entity}}的显示名",
|
||||
"update-profiler-settings": "Update profiler setting.",
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
formatDateTimeFromSeconds,
|
||||
formatDateTimeLong,
|
||||
formatTimeDurationFromSeconds,
|
||||
isValidDateFormat,
|
||||
} from './DateTimeUtils';
|
||||
|
||||
const systemLocale = Settings.defaultLocale;
|
||||
@ -62,3 +63,23 @@ describe('DateTimeUtils tests', () => {
|
||||
expect(customFormatDateTime(0, 'yyyy/MM/dd')).toBe(`1970/01/01`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Date and DateTime Format Validation', () => {
|
||||
it('isValidDateFormat should validate date format correctly', () => {
|
||||
expect(isValidDateFormat('yyyy-MM-dd')).toBe(true);
|
||||
expect(isValidDateFormat('dd-MM-yyyy')).toBe(true);
|
||||
expect(isValidDateFormat('MM/dd/yyyy')).toBe(true);
|
||||
expect(isValidDateFormat('dd/MM/yyyy')).toBe(true);
|
||||
expect(isValidDateFormat('yyyy/MM/dd')).toBe(true);
|
||||
expect(isValidDateFormat('invalid-format')).toBe(false);
|
||||
});
|
||||
|
||||
it('isValidDateFormat should validate dateTime format correctly', () => {
|
||||
expect(isValidDateFormat('yyyy-MM-dd HH:mm:ss')).toBe(true);
|
||||
expect(isValidDateFormat('dd-MM-yyyy HH:mm:ss')).toBe(true);
|
||||
expect(isValidDateFormat('MM/dd/yyyy HH:mm:ss')).toBe(true);
|
||||
expect(isValidDateFormat('dd/MM/yyyy HH:mm:ss')).toBe(true);
|
||||
expect(isValidDateFormat('yyyy/MM/dd HH:mm:ss')).toBe(true);
|
||||
expect(isValidDateFormat('invalid-format')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -187,3 +187,13 @@ export const getDaysRemaining = (timestamp: number) =>
|
||||
toInteger(
|
||||
-DateTime.now().diff(DateTime.fromMillis(timestamp), ['days']).days
|
||||
);
|
||||
|
||||
export const isValidDateFormat = (format: string) => {
|
||||
try {
|
||||
const dt = DateTime.fromFormat(DateTime.now().toFormat(format), format);
|
||||
|
||||
return dt.isValid;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user