fix(#15683): backend validation error for the following property types (#16881)

* fix(#15683): backend validation error for the following property types

* feat: Update supported date and datetime formats

* test: add e2e playwright test for custom properties

* chore: remove cypress test for custom properties

* Add date, dateTime, time backend support for custom property

* fix: update custom property display names in AddCustomProperty component

* feat: Update supported date and time formats

* chore: Update supported date and time formats

* feat: Update date and time formats in CustomProperty

* test: add playwright test

* Update supported date and time formats

* feat: Add tests for custom properties in Playwright in 2 parts

* Update supported date and time formats

* Update supported date and time formats

* chore: Update date format to uppercase in PropertyValue component

* feat: Add support for custom date format in PropertyValue component

* Update supported date and time formats

* test: add playwright test for time, dateTime, timeInterval and date properties

* Update supported time formats

* chore: Add focus to time and date pickers before filling values

* chore: Add focus to time and date pickers before filling values

* Remove date, dateTime, time from type_entity

---------

Co-authored-by: sonikashah <sonikashah94@gmail.com>
This commit is contained in:
Sachin Chaurasiya 2024-07-15 11:37:38 +05:30 committed by GitHub
parent d27f518f26
commit 6e99fe7bda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1028 additions and 1533 deletions

View File

@ -75,3 +75,7 @@ CREATE TABLE IF NOT EXISTS api_endpoint_entity (
UNIQUE (fqnHash),
INDEX (name)
);
-- Remove date, dateTime, time from type_entity, as they are no more om-field-types, instead we have date-cp, time-cp, dateTime-cp as om-field-types
DELETE FROM type_entity
WHERE name IN ('date', 'dateTime', 'time');

View File

@ -69,3 +69,7 @@ CREATE TABLE IF NOT EXISTS api_endpoint_entity (
PRIMARY KEY (id),
UNIQUE (fqnHash)
);
-- Remove date, dateTime, time from type_entity, as they are no more om-field-types, instead we have date-cp, time-cp, dateTime-cp as om-field-types
DELETE FROM type_entity
WHERE name IN ('date', 'dateTime', 'time');

View File

@ -102,4 +102,30 @@ public class TypeRegistry {
public static String getPropertyName(String propertyFQN) {
return FullyQualifiedName.split(propertyFQN)[2];
}
public static String getCustomPropertyType(String entityType, String propertyName) {
Type type = TypeRegistry.TYPES.get(entityType);
if (type != null && type.getCustomProperties() != null) {
for (CustomProperty property : type.getCustomProperties()) {
if (property.getName().equals(propertyName)) {
return property.getPropertyType().getName();
}
}
}
return null;
}
public static String getCustomPropertyConfig(String entityType, String propertyName) {
Type type = TypeRegistry.TYPES.get(entityType);
if (type != null && type.getCustomProperties() != null) {
for (CustomProperty property : type.getCustomProperties()) {
if (property.getName().equals(propertyName)
&& property.getCustomPropertyConfig() != null
&& property.getCustomPropertyConfig().getConfig() != null) {
return property.getCustomPropertyConfig().getConfig().toString();
}
}
}
return null;
}
}

View File

@ -237,6 +237,11 @@ public final class CatalogExceptionMessage {
return String.format("Unknown custom field %s", fieldName);
}
public static String dateTimeValidationError(String fieldName, String format) {
return String.format(
"Custom field %s value is not as per defined format %s", fieldName, format);
}
public static String jsonValidationError(String fieldName, String validationMessages) {
return String.format("Custom field %s has invalid JSON %s", fieldName, validationMessages);
}

View File

@ -77,11 +77,17 @@ import com.networknt.schema.JsonSchema;
import com.networknt.schema.ValidationMessage;
import java.io.IOException;
import java.net.URI;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
@ -1340,6 +1346,35 @@ public abstract class EntityRepository<T extends EntityInterface> {
if (jsonSchema == null) {
throw new IllegalArgumentException(CatalogExceptionMessage.unknownCustomField(fieldName));
}
String customPropertyType = TypeRegistry.getCustomPropertyType(entityType, fieldName);
String propertyConfig = TypeRegistry.getCustomPropertyConfig(entityType, fieldName);
DateTimeFormatter formatter = null;
try {
if ("date-cp".equals(customPropertyType)) {
DateTimeFormatter inputFormatter =
DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig), Locale.ENGLISH);
// Parse the input string into a TemporalAccessor
TemporalAccessor date = inputFormatter.parse(fieldValue.textValue());
// Create a formatter for the desired output format
DateTimeFormatter outputFormatter =
DateTimeFormatter.ofPattern(propertyConfig, Locale.ENGLISH);
((ObjectNode) jsonNode).put(fieldName, outputFormatter.format(date));
} else if ("dateTime-cp".equals(customPropertyType)) {
formatter = DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig));
LocalDateTime dateTime = LocalDateTime.parse(fieldValue.textValue(), formatter);
((ObjectNode) jsonNode).put(fieldName, dateTime.format(formatter));
} else if ("time-cp".equals(customPropertyType)) {
formatter = DateTimeFormatter.ofPattern(Objects.requireNonNull(propertyConfig));
LocalTime time = LocalTime.parse(fieldValue.textValue(), formatter);
((ObjectNode) jsonNode).put(fieldName, time.format(formatter));
}
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(
CatalogExceptionMessage.dateTimeValidationError(
fieldName, TypeRegistry.getCustomPropertyConfig(entityType, fieldName)));
}
Set<ValidationMessage> validationMessages = jsonSchema.validate(fieldValue);
if (!validationMessages.isEmpty()) {
throw new IllegalArgumentException(

View File

@ -22,9 +22,11 @@ import static org.openmetadata.service.Entity.FIELD_DESCRIPTION;
import static org.openmetadata.service.util.EntityUtil.customFieldMatch;
import static org.openmetadata.service.util.EntityUtil.getCustomField;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j;
@ -185,10 +187,48 @@ public class TypeRepository extends EntityRepository<Type> {
throw new IllegalArgumentException("Enum Custom Property Type must have EnumConfig.");
}
}
case "date-cp" -> validateDateFormat(
customProperty.getCustomPropertyConfig(), getDateTokens(), "Invalid date format");
case "dateTime-cp" -> validateDateFormat(
customProperty.getCustomPropertyConfig(), getDateTimeTokens(), "Invalid dateTime format");
case "time-cp" -> validateDateFormat(
customProperty.getCustomPropertyConfig(), getTimeTokens(), "Invalid time format");
case "int", "string" -> {}
}
}
private void validateDateFormat(
CustomPropertyConfig config, Set<Character> validTokens, String errorMessage) {
if (config != null) {
String format = String.valueOf(config.getConfig());
for (char c : format.toCharArray()) {
if (Character.isLetter(c) && !validTokens.contains(c)) {
throw new IllegalArgumentException(errorMessage + ": " + format);
}
}
try {
DateTimeFormatter.ofPattern(format);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(errorMessage + ": " + format, e);
}
} else {
throw new IllegalArgumentException(errorMessage + " must have Config populated with format.");
}
}
private Set<Character> getDateTokens() {
return Set.of('y', 'M', 'd', 'E', 'D', 'W', 'w');
}
private Set<Character> getDateTimeTokens() {
return Set.of(
'y', 'M', 'd', 'E', 'D', 'W', 'w', 'H', 'h', 'm', 's', 'a', 'T', 'X', 'Z', '+', '-', 'S');
}
private Set<Character> getTimeTokens() {
return Set.of('H', 'h', 'm', 's', 'a', 'S');
}
/** Handles entity updated from PUT and POST operation. */
public class TypeUpdater extends EntityUpdater {
public TypeUpdater(Type original, Type updated, Operation operation) {

View File

@ -68,23 +68,35 @@
"type": "string"
},
"date": {
"$comment" : "@om-field-type",
"description": "Date in ISO 8601 format in UTC. Example - '2018-11-13'.",
"type": "string",
"format": "date"
},
"dateTime": {
"$comment" : "@om-field-type",
"description": "Date and time in ISO 8601 format. Example - '2018-11-13T20:20:39+00:00'.",
"type": "string",
"format": "date-time"
},
"time": {
"$comment" : "@om-field-type",
"description": "time in ISO 8601 format. Example - '20:20:39+00:00'.",
"type": "string",
"format": "time"
},
"date-cp": {
"$comment" : "@om-field-type",
"description": "Date as defined in custom property.",
"type": "string"
},
"dateTime-cp": {
"$comment" : "@om-field-type",
"description": "Date and time as defined in custom property.",
"type": "string"
},
"time-cp": {
"$comment" : "@om-field-type",
"description": "Time as defined in custom property.",
"type": "string"
},
"enum": {
"$comment" : "@om-field-type",
"description": "List of values in Enum.",

View File

@ -1,654 +0,0 @@
/*
* Copyright 2023 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
CUSTOM_PROPERTY_INVALID_NAMES,
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR,
} from '../../constants/constants';
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',
ENTITY_REFERENCE = 'entityReference',
ENTITY_REFERENCE_LIST = 'entityReferenceList',
}
export interface CustomProperty {
name: string;
type: CustomPropertyType;
description: string;
propertyType: {
name: string;
type: string;
};
}
export const generateCustomProperty = (type: CustomPropertyType) => ({
name: `cypress${type.toLowerCase()}${Date.now()}`,
type,
description: `${type} cypress Property`,
});
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__',
};
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',
};
case 'timestamp':
return {
value: '1710831125922',
newValue: '1710831125923',
};
case 'entityReference':
return {
value: 'Adam Matthews',
newValue: 'Amber Green',
};
case 'entityReferenceList':
return {
value: 'Aaron Johnson,Organization',
newValue: 'Aaron Warren',
};
default:
return {
value: '',
newValue: '',
};
}
};
export const deleteCustomPropertyForEntity = ({
property,
type,
}: {
property: CustomProperty;
type: EntityType;
}) => {
interceptURL('GET', `/api/v1/metadata/types/name/*`, 'getEntity');
interceptURL('PATCH', `/api/v1/metadata/types/*`, 'patchEntity');
// Selecting the entity
cy.settingClick(type, true);
verifyResponseStatusCode('@getEntity', 200);
cy.get(
`[data-row-key="${property.name}"] [data-testid="delete-button"]`
).click();
cy.get('[data-testid="modal-header"]').should('contain', property.name);
cy.get('[data-testid="save-button"]').click();
verifyResponseStatusCode('@patchEntity', 200);
};
export const setValueForProperty = (
propertyName: string,
value: string,
propertyType: string
) => {
cy.get('[data-testid="custom_properties"]').click();
cy.get('tbody').should('contain', propertyName);
// Adding value for the custom property
// Navigating through the created custom property for adding value
cy.get(`[data-row-key="${propertyName}"]`)
.find('[data-testid="edit-icon"]')
.scrollIntoView()
.as('editbutton');
cy.get('@editbutton').should('be.visible').click({ force: true });
interceptURL('PATCH', `/api/v1/*/*`, 'patchEntity');
// Checking for value text box or markdown box
switch (propertyType) {
case 'markdown':
cy.get('.toastui-editor-md-container > .toastui-editor > .ProseMirror')
.should('be.visible')
.clear()
.type(value);
cy.get('[data-testid="save"]').click();
break;
case 'email':
cy.get('[data-testid="email-input"]')
.should('be.visible')
.clear()
.type(value);
cy.get('[data-testid="inline-save-btn"]').click();
break;
case 'duration':
cy.get('[data-testid="duration-input"]')
.should('be.visible')
.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"]')
.should('be.visible')
.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"]')
.should('be.visible')
.clear()
.type(startValue);
cy.get('[data-testid="end-input"]')
.should('be.visible')
.clear()
.type(endValue);
cy.get('[data-testid="inline-save-btn"]').click();
break;
}
case 'string':
case 'integer':
case 'number':
cy.get('[data-testid="value-input"]')
.should('be.visible')
.clear()
.type(value);
cy.get('[data-testid="inline-save-btn"]').click();
break;
case 'entityReference':
case 'entityReferenceList': {
const refValues = value.split(',');
refValues.forEach((val) => {
interceptURL(
'GET',
`/api/v1/search/query?q=*${encodeURIComponent(val)}*`,
'searchEntityReference'
);
cy.get('#entityReference').clear().type(`${val}`);
cy.wait('@searchEntityReference');
cy.get(`[data-testid="${val}"]`).click();
});
cy.get('[data-testid="inline-save-btn"]').click();
break;
}
default:
break;
}
verifyResponseStatusCode('@patchEntity', 200);
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 if (
['entityReference', 'entityReferenceList'].includes(propertyType)
) {
// do nothing
} else {
cy.get(`[data-row-key="${propertyName}"]`).should(
'contain',
value.replace(/\*|_/gi, '')
);
}
};
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"]',
{
timeout: 10000,
}
).scrollIntoView();
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 if (
['entityReference', 'entityReferenceList'].includes(propertyType)
) {
// do nothing
} else {
cy.get(`[data-row-key="${propertyName}"]`).should(
'contain',
value.replace(/\*|_/gi, '')
);
}
};
export const generateCustomProperties = () => {
return {
name: `cyCustomProperty${uuid()}`,
description: `cyCustomProperty${uuid()}`,
};
};
export const verifyCustomPropertyRows = () => {
cy.get('[data-testid="custom_properties"]').click();
cy.get('.ant-table-row').should('have.length.gte', 10);
cy.get('.ant-tabs-tab').first().click();
cy.get(
'[data-testid="entity-right-panel"] [data-testid="custom-properties-table"]',
{
timeout: 10000,
}
).scrollIntoView();
cy.get(
'[data-testid="entity-right-panel"] [data-testid="custom-properties-table"] tbody tr'
).should('have.length', 6);
};
export const deleteCustomProperties = (
tableSchemaId: string,
token: string
) => {
cy.request({
method: 'PATCH',
url: `/api/v1/metadata/types/${tableSchemaId}`,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json-patch+json',
},
body: [
{
op: 'remove',
path: '/customProperties',
},
],
});
};
export const customPropertiesArray = Array(10)
.fill(null)
.map(() => generateCustomProperties());
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[];
}) => {
// eslint-disable-next-line max-len
const longDescription = `Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolore neque fuga reprehenderit placeat, sint doloremque quo expedita consequatur fugiat maxime maiores voluptate eum quis quas dignissimos cumque perspiciatis optio dolorem blanditiis iure natus commodi dolor quam. Voluptatem excepturi aut, at ullam aliquid repudiandae distinctio ipsam voluptates tenetur a. Sit, illum.`;
// Add Custom property for selected entity
cy.get('[data-testid="add-field-button"]').click();
// validation should work
cy.get('[data-testid="create-button"]').scrollIntoView().click();
cy.get('#name_help').should('contain', 'Name is required');
cy.get('#propertyType_help').should('contain', 'Property Type is required');
cy.get('#description_help').should('contain', 'Description is required');
// capital case validation
cy.get('[data-testid="name"]')
.scrollIntoView()
.type(CUSTOM_PROPERTY_INVALID_NAMES.CAPITAL_CASE);
cy.get('[role="alert"]').should(
'contain',
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR
);
// with underscore validation
cy.get('[data-testid="name"]')
.clear()
.type(CUSTOM_PROPERTY_INVALID_NAMES.WITH_UNDERSCORE);
cy.get('[role="alert"]').should(
'contain',
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR
);
// with space validation
cy.get('[data-testid="name"]')
.clear()
.type(CUSTOM_PROPERTY_INVALID_NAMES.WITH_SPACE);
cy.get('[role="alert"]').should(
'contain',
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR
);
// with dots validation
cy.get('[data-testid="name"]')
.clear()
.type(CUSTOM_PROPERTY_INVALID_NAMES.WITH_DOTS);
cy.get('[role="alert"]').should(
'contain',
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR
);
// should allow name in another languages
cy.get('[data-testid="name"]').clear().type('汝らヴェディア');
// should not throw the validation error
cy.get('#name_help').should('not.exist');
cy.get('[data-testid="name"]').clear().type(propertyName);
cy.get(`#root\\/propertyType`).clear().type(customType);
cy.get(`[title="${customType}"]`).click();
if (customType === 'Enum') {
enumConfig.values.forEach((val) => {
cy.get('#root\\/enumConfig').type(`${val}{enter}`);
});
cy.clickOutside();
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} ${longDescription}`);
// 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', propertyName);
// Navigating to home page
cy.clickOnLogo();
};
export const editCreatedProperty = (propertyName: string, type?: string) => {
// Fetching for edit button
cy.get(`[data-row-key="${propertyName}"]`)
.find('[data-testid="edit-button"]')
.as('editButton');
if (type === 'Enum') {
cy.get(`[data-row-key="${propertyName}"]`)
.find('[data-testid="enum-config"]')
.should('contain', '["enum1","enum2","enum3"]');
}
cy.get('@editButton').click();
cy.get(descriptionBox).clear().type('This is new description');
if (type === 'Enum') {
cy.get('#root\\/customPropertyConfig').type(`updatedValue{enter}`);
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();
/**
* @link https://docs.cypress.io/guides/references/configuration#Timeouts
* default responseTimeout is 30000ms which is not enough for the patch request
* so we need to increase the responseTimeout to 70000ms for AUT environment in PATCH request
*/
cy.wait('@checkPatchForDescription', { responseTimeout: 70000 });
cy.get('.ant-modal-wrap').should('not.exist');
// Fetching for updated descriptions for the created custom property
cy.get(`[data-row-key="${propertyName}"]`)
.find('[data-testid="viewer-container"]')
.should('contain', 'This is new description');
if (type === 'Enum') {
cy.get(`[data-row-key="${propertyName}"]`)
.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) => {
// Fetching for delete button
cy.get(`[data-row-key="${propertyName}"]`)
.scrollIntoView()
.find('[data-testid="delete-button"]')
.click();
// Checking property name is present on the delete pop-up
cy.get('[data-testid="body-text"]').should('contain', propertyName);
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'],
},
},
}
: {}),
...(['entityReference', 'entityReferenceList'].includes(
item.name
)
? {
customPropertyConfig: {
config: ['user', 'team'],
},
}
: {}),
},
})
.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;
}
>
);
});
});
});
});
});
};

View File

@ -1,32 +0,0 @@
/*
* 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.
*/
import { EntityType } from './Entity.interface';
export const CustomPropertySupportedEntityList = [
EntityType.Database,
EntityType.DatabaseSchema,
EntityType.Table,
EntityType.StoreProcedure,
EntityType.Topic,
EntityType.Dashboard,
EntityType.Pipeline,
EntityType.Container,
EntityType.MlModel,
EntityType.GlossaryTerm,
EntityType.SearchIndex,
];
export const ENTITY_REFERENCE_PROPERTIES = [
'Entity Reference',
'Entity Reference List',
];

View File

@ -337,195 +337,6 @@ export const ENTITY_SERVICE_TYPE = {
Search: 'Search',
};
export const ENTITIES = {
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,
},
dateFormatConfig: 'yyyy-mm-dd',
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
entityReferenceConfig: ['User', 'Team'],
entityObj: {},
entityApiType: 'containers',
},
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,
},
dateFormatConfig: 'yyyy-mm-dd',
dateTimeFormatConfig: 'yyyy-mm-dd hh:mm:ss',
entityReferenceConfig: ['User', 'Team'],
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',
integerValue: '78',
stringValue: 'This is string propery',
markdownValue: 'This is markdown value',
enumConfig: {
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 = {
username: 'admin@openmetadata.org',
password: 'admin',
@ -594,16 +405,6 @@ export const DESTINATION = {
},
};
export const CUSTOM_PROPERTY_INVALID_NAMES = {
CAPITAL_CASE: 'CapitalCase',
WITH_UNDERSCORE: 'with_underscore',
WITH_DOTS: 'with.',
WITH_SPACE: 'with ',
};
export const CUSTOM_PROPERTY_NAME_VALIDATION_ERROR =
'Name must start with lower case with no space, underscore, or dots.';
export const TAG_INVALID_NAMES = {
MIN_LENGTH: 'c',
MAX_LENGTH: 'a87439625b1c2d3e4f5061728394a5b6c7d8e90a1b2c3d4e5f67890ab',

View File

@ -1,577 +0,0 @@
/*
* Copyright 2022 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { lowerCase } from 'lodash';
import { interceptURL, verifyResponseStatusCode } from '../../common/common';
import {
createGlossary,
createGlossaryTerms,
deleteGlossary,
} from '../../common/GlossaryUtils';
import {
addCustomPropertiesForEntity,
CustomProperty,
CustomPropertyType,
deleteCreatedProperty,
deleteCustomPropertyForEntity,
editCreatedProperty,
generateCustomProperty,
setValueForProperty,
validateValueForProperty,
} from '../../common/Utils/CustomProperty';
import { visitEntityDetailsPage } from '../../common/Utils/Entity';
import { updateJWTTokenExpiryTime } from '../../common/Utils/Login';
import { ENTITIES, JWT_EXPIRY_TIME_MAP, uuid } from '../../constants/constants';
import { EntityType, SidebarItem } from '../../constants/Entity.interface';
import { GLOSSARY_1 } from '../../constants/glossary.constant';
const glossaryTerm = {
name: 'glossaryTerm',
description: 'This is Glossary Term custom property',
integerValue: '45',
stringValue: 'This is string property',
markdownValue: 'This is markdown value',
entityApiType: 'glossaryTerm',
};
const customPropertyValue = {
Integer: {
value: '123',
newValue: '456',
property: generateCustomProperty(CustomPropertyType.INTEGER),
},
String: {
value: '123',
newValue: '456',
property: generateCustomProperty(CustomPropertyType.STRING),
},
Markdown: {
value: '**Bold statement**',
newValue: '__Italic statement__',
property: generateCustomProperty(CustomPropertyType.MARKDOWN),
},
};
describe('Custom Properties should work properly', { tags: 'Settings' }, () => {
before(() => {
cy.login();
updateJWTTokenExpiryTime(JWT_EXPIRY_TIME_MAP['2 hours']);
});
after(() => {
cy.login();
updateJWTTokenExpiryTime(JWT_EXPIRY_TIME_MAP['1 hour']);
});
beforeEach(() => {
cy.login();
});
afterEach(() => {
cy.logout();
});
[
'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/Update/Delete ${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);
// Getting the property
addCustomPropertiesForEntity({
propertyName,
customPropertyData: entity,
customType: type,
});
// Navigating back to custom properties page
cy.settingClick(entity.entityApiType, true);
verifyResponseStatusCode('@getEntity', 200);
// `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);
// `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 Enum custom properties', () => {
Object.values(ENTITIES).forEach((entity) => {
const propertyName = `addcyentity${entity.name}test${uuid()}`;
it(`Add/Update/Delete Enum 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: 'Enum',
enumConfig: entity.enumConfig,
});
// Navigating back to custom properties page
cy.settingClick(entity.entityApiType, true);
verifyResponseStatusCode('@getEntity', 200);
// `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, 'Enum');
// `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 custom properties', () => {
Object.values(ENTITIES).forEach((entity) => {
const propertyName = `addcyentity${entity.name}test${uuid()}`;
it(`Add/Update/Delete 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);
// `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');
// `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/Update/Delete 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);
// `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');
// `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);
});
});
});
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('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);
});
});
});
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('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(', ');
it('test custom properties in advanced search modal', () => {
cy.settingClick(glossaryTerm.entityApiType, true);
addCustomPropertiesForEntity({
propertyName,
customPropertyData: glossaryTerm,
customType: 'Integer',
});
// Navigating to explore page
cy.sidebarClick(SidebarItem.EXPLORE);
interceptURL(
'GET',
`/api/v1/metadata/types/name/glossaryTerm*`,
'getEntity'
);
cy.get(
`[data-testid=${Cypress.$.escapeSelector('glossary terms-tab')}]`
).click();
cy.get('[data-testid="advance-search-button"]').click();
verifyResponseStatusCode('@getEntity', 200);
// Click on field dropdown
cy.get('.rule--field > .ant-select > .ant-select-selector').eq(0).click();
// Select custom property fields
cy.get(`[title="Custom Properties"]`).eq(0).click();
// Click on field dropdown
cy.get('.rule--field > .ant-select > .ant-select-selector').eq(0).click();
// Verify field exists
cy.get(`[title="${propertyName}"]`).should('be.visible');
cy.get('[data-testid="cancel-btn"]').click();
});
it(`Delete created property for glossary term entity`, () => {
interceptURL(
'GET',
`/api/v1/metadata/types/name/${glossaryTerm.name}*`,
'getEntity'
);
// Selecting the entity
cy.settingClick(glossaryTerm.entityApiType, true);
verifyResponseStatusCode('@getEntity', 200);
deleteCreatedProperty(propertyName);
});
// TODO: Need to fix this for mysql due to data issue @Sachin-chaurasiya
// eslint-disable-next-line jest/no-disabled-tests
it.skip(`Add update and delete ${properties} custom properties for glossary term `, () => {
interceptURL('GET', '/api/v1/glossaryTerms*', 'getGlossaryTerms');
interceptURL('GET', '/api/v1/glossaries?fields=*', 'fetchGlossaries');
cy.sidebarClick(SidebarItem.GLOSSARY);
const glossary = GLOSSARY_1;
glossary.terms = [GLOSSARY_1.terms[0]];
createGlossary(GLOSSARY_1, false);
createGlossaryTerms(glossary);
cy.settingClick(glossaryTerm.entityApiType, true);
Object.values(CustomPropertyType).forEach((type) => {
addCustomPropertiesForEntity({
propertyName: lowerCase(type),
customPropertyData: glossaryTerm,
customType: type,
});
cy.settingClick(glossaryTerm.entityApiType, true);
});
visitEntityDetailsPage({
term: glossary.terms[0].name,
serviceName: glossary.terms[0].fullyQualifiedName,
entity: 'glossaryTerms' as EntityType,
dataTestId: `${glossary.name}-${glossary.terms[0].name}`,
});
// set custom property value
Object.values(CustomPropertyType).forEach((type) => {
setValueForProperty(
lowerCase(type),
customPropertyValue[type].value,
lowerCase(type)
);
validateValueForProperty(
lowerCase(type),
customPropertyValue[type].value,
lowerCase(type)
);
});
// update custom property value
Object.values(CustomPropertyType).forEach((type) => {
setValueForProperty(
lowerCase(type),
customPropertyValue[type].newValue,
lowerCase(type)
);
validateValueForProperty(
lowerCase(type),
customPropertyValue[type].newValue,
lowerCase(type)
);
});
// delete custom properties
Object.values(CustomPropertyType).forEach((customPropertyType) => {
const type = glossaryTerm.entityApiType as EntityType;
const property = customPropertyValue[customPropertyType].property ?? {};
deleteCustomPropertyForEntity({
property: {
...property,
name: lowerCase(customPropertyType),
} as CustomProperty,
type,
});
});
// delete glossary and glossary term
cy.sidebarClick(SidebarItem.GLOSSARY);
deleteGlossary(glossary.name);
});
});
});

View File

@ -32,3 +32,213 @@ export const ENTITY_REFERENCE_PROPERTIES = [
'Entity Reference',
'Entity Reference List',
];
export const CUSTOM_PROPERTIES_ENTITIES = {
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,
},
dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss',
entityReferenceConfig: ['User', 'Team'],
entityObj: {},
entityApiType: 'containers',
},
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,
},
dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss',
entityReferenceConfig: ['User', 'Team'],
entityObj: {},
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',
timeFormatConfig: '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',
timeFormatConfig: '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',
timeFormatConfig: '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',
timeFormatConfig: 'HH:mm:ss',
entityReferenceConfig: ['User', 'Team'],
entityObj: {},
entityApiType: 'mlmodels',
},
entity_pipeline: {
name: 'pipeline',
description: 'This is Pipeline custom property',
integerValue: '78',
stringValue: 'This is string propery',
markdownValue: 'This is markdown value',
enumConfig: {
values: ['enum1', 'enum2', 'enum3'],
multiSelect: true,
},
dateFormatConfig: 'yyyy-MM-dd',
dateTimeFormatConfig: 'yyyy-MM-dd HH:mm:ss',
timeFormatConfig: 'HH:mm:ss',
entityReferenceConfig: ['User', 'Team'],
entityObj: {},
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',
timeFormatConfig: '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',
timeFormatConfig: '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',
timeFormatConfig: 'HH:mm:ss',
entityReferenceConfig: ['User', 'Team'],
entityObj: {},
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',
timeFormatConfig: 'HH:mm:ss',
entityReferenceConfig: ['User', 'Team'],
entityObj: {},
entityApiType: 'topics',
},
};
export const CUSTOM_PROPERTY_INVALID_NAMES = {
CAPITAL_CASE: 'CapitalCase',
WITH_UNDERSCORE: 'with_underscore',
WITH_DOTS: 'with.',
WITH_SPACE: 'with ',
};
export const CUSTOM_PROPERTY_NAME_VALIDATION_ERROR =
'Name must start with lower case with no space, underscore, or dots.';

View File

@ -0,0 +1,69 @@
/*
* 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.
*/
import test from '@playwright/test';
import { CUSTOM_PROPERTIES_ENTITIES } from '../../constant/customProperty';
import { redirectToHomePage, uuid } from '../../utils/common';
import {
addCustomPropertiesForEntity,
deleteCreatedProperty,
editCreatedProperty,
} from '../../utils/customProperty';
import { settingClick } from '../../utils/sidebar';
const propertiesList = [
'Integer',
'String',
'Markdown',
'Duration',
'Email',
'Number',
'Sql Query',
'Time Interval',
'Timestamp',
];
// use the admin user to login
test.use({ storageState: 'playwright/.auth/admin.json' });
test.describe('Custom properties without custom property config', () => {
test.beforeEach('Visit Home Page', async ({ page }) => {
await redirectToHomePage(page);
});
propertiesList.forEach((property) => {
test.describe(`Add update and delete ${property} custom properties`, () => {
Object.values(CUSTOM_PROPERTIES_ENTITIES).forEach(async (entity) => {
const propertyName = `pwcustomproperty${entity.name}test${uuid()}`;
test(`Add ${property} custom property for ${entity.name}`, async ({
page,
}) => {
test.slow(true);
await settingClick(page, entity.entityApiType, true);
await addCustomPropertiesForEntity({
page,
propertyName,
customPropertyData: entity,
customType: property,
});
await editCreatedProperty(page, propertyName);
await deleteCreatedProperty(page, propertyName);
});
});
});
});
});

View File

@ -0,0 +1,186 @@
/*
* 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.
*/
import test from '@playwright/test';
import { CUSTOM_PROPERTIES_ENTITIES } from '../../constant/customProperty';
import { redirectToHomePage, uuid } from '../../utils/common';
import {
addCustomPropertiesForEntity,
deleteCreatedProperty,
editCreatedProperty,
} from '../../utils/customProperty';
import { settingClick } from '../../utils/sidebar';
// use the admin user to login
test.use({ storageState: 'playwright/.auth/admin.json' });
test.describe('Custom properties with custom property config', () => {
test.beforeEach('Visit Home Page', async ({ page }) => {
await redirectToHomePage(page);
});
test.describe('Add update and delete Enum custom properties', () => {
Object.values(CUSTOM_PROPERTIES_ENTITIES).forEach(async (entity) => {
const propertyName = `pwcustomproperty${entity.name}test${uuid()}`;
test(`Add Enum custom property for ${entity.name}`, async ({ page }) => {
test.slow(true);
await settingClick(page, entity.entityApiType, true);
await addCustomPropertiesForEntity({
page,
propertyName,
customPropertyData: entity,
customType: 'Enum',
enumConfig: entity.enumConfig,
});
await editCreatedProperty(page, propertyName);
await deleteCreatedProperty(page, propertyName);
});
});
});
test.describe(
'Add update and delete Entity Reference custom properties',
() => {
Object.values(CUSTOM_PROPERTIES_ENTITIES).forEach(async (entity) => {
const propertyName = `pwcustomproperty${entity.name}test${uuid()}`;
test(`Add Entity Reference custom property for ${entity.name}`, async ({
page,
}) => {
test.slow(true);
await settingClick(page, entity.entityApiType, true);
await addCustomPropertiesForEntity({
page,
propertyName,
customPropertyData: entity,
customType: 'Entity Reference',
entityReferenceConfig: entity.entityReferenceConfig,
});
await editCreatedProperty(page, propertyName);
await deleteCreatedProperty(page, propertyName);
});
});
}
);
test.describe(
'Add update and delete Entity Reference List custom properties',
() => {
Object.values(CUSTOM_PROPERTIES_ENTITIES).forEach(async (entity) => {
const propertyName = `pwcustomproperty${entity.name}test${uuid()}`;
test(`Add Entity Reference list custom property for ${entity.name}`, async ({
page,
}) => {
test.slow(true);
await settingClick(page, entity.entityApiType, true);
await addCustomPropertiesForEntity({
page,
propertyName,
customPropertyData: entity,
customType: 'Entity Reference List',
entityReferenceConfig: entity.entityReferenceConfig,
});
await editCreatedProperty(page, propertyName);
await deleteCreatedProperty(page, propertyName);
});
});
}
);
test.describe('Add update and delete Date custom properties', () => {
Object.values(CUSTOM_PROPERTIES_ENTITIES).forEach(async (entity) => {
const propertyName = `pwcustomproperty${entity.name}test${uuid()}`;
test(`Add Date custom property for ${entity.name}`, async ({ page }) => {
test.slow(true);
await settingClick(page, entity.entityApiType, true);
await addCustomPropertiesForEntity({
page,
propertyName,
customPropertyData: entity,
customType: 'Date',
formatConfig: entity.dateFormatConfig,
});
await editCreatedProperty(page, propertyName);
await deleteCreatedProperty(page, propertyName);
});
});
});
test.describe('Add update and delete Time custom properties', () => {
Object.values(CUSTOM_PROPERTIES_ENTITIES).forEach(async (entity) => {
const propertyName = `pwcustomproperty${entity.name}test${uuid()}`;
test(`Add Time custom property for ${entity.name}`, async ({ page }) => {
test.slow(true);
await settingClick(page, entity.entityApiType, true);
await addCustomPropertiesForEntity({
page,
propertyName,
customPropertyData: entity,
customType: 'Time',
formatConfig: entity.timeFormatConfig,
});
await editCreatedProperty(page, propertyName);
await deleteCreatedProperty(page, propertyName);
});
});
});
test.describe('Add update and delete DateTime custom properties', () => {
Object.values(CUSTOM_PROPERTIES_ENTITIES).forEach(async (entity) => {
const propertyName = `pwcustomproperty${entity.name}test${uuid()}`;
test(`Add DateTime custom property for ${entity.name}`, async ({
page,
}) => {
test.slow(true);
await settingClick(page, entity.entityApiType, true);
await addCustomPropertiesForEntity({
page,
propertyName,
customPropertyData: entity,
customType: 'Date Time',
formatConfig: entity.dateTimeFormatConfig,
});
await editCreatedProperty(page, propertyName);
await deleteCreatedProperty(page, propertyName);
});
});
});
});

View File

@ -11,12 +11,17 @@
* limitations under the License.
*/
import { APIRequestContext, expect, Page } from '@playwright/test';
import {
CUSTOM_PROPERTY_INVALID_NAMES,
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR,
ENTITY_REFERENCE_PROPERTIES,
} from '../constant/customProperty';
import {
EntityTypeEndpoint,
ENTITY_PATH,
} from '../support/entity/Entity.interface';
import { UserClass } from '../support/user/UserClass';
import { uuid } from './common';
import { descriptionBox, uuid } from './common';
export enum CustomPropertyType {
STRING = 'String',
@ -35,6 +40,10 @@ export enum CustomPropertyTypeByName {
TIMESTAMP = 'timestamp',
ENTITY_REFERENCE = 'entityReference',
ENTITY_REFERENCE_LIST = 'entityReferenceList',
TIME_INTERVAL = 'timeInterval',
TIME_CP = 'time-cp',
DATE_CP = 'date-cp',
DATE_TIME_CP = 'dateTime-cp',
}
export interface CustomProperty {
@ -132,6 +141,31 @@ export const setValueForProperty = async (data: {
break;
}
case 'time-cp': {
await page.locator('[data-testid="time-picker"]').isVisible();
await page.locator('[data-testid="time-picker"]').click();
await page.locator('[data-testid="time-picker"]').fill(value);
await page.getByRole('button', { name: 'OK', exact: true }).click();
await page.locator('[data-testid="inline-save-btn"]').click();
break;
}
case 'date-cp':
case 'dateTime-cp': {
await page.locator('[data-testid="date-time-picker"]').isVisible();
await page.locator('[data-testid="date-time-picker"]').click();
await page.locator('[data-testid="date-time-picker"]').fill(value);
if (propertyType === 'dateTime-cp') {
await page.getByText('Now', { exact: true }).click();
} else {
await page.getByText('Today', { exact: true }).click();
}
await page.locator('[data-testid="inline-save-btn"]').click();
break;
}
case 'string':
case 'integer':
case 'number':
@ -191,7 +225,12 @@ export const validateValueForProperty = async (data: {
page.getByLabel('Custom Properties').locator('.CodeMirror-scroll')
).toContainText(value);
} else if (
!['entityReference', 'entityReferenceList'].includes(propertyType)
![
'entityReference',
'entityReferenceList',
'date-cp',
'dateTime-cp',
].includes(propertyType)
) {
await expect(page.getByRole('row', { name: propertyName })).toContainText(
value.replace(/\*|_/gi, '')
@ -263,6 +302,30 @@ export const getPropertyValues = (
newValue: users.user4,
};
case 'timeInterval':
return {
value: '1710831125922,1710831125924',
newValue: '1710831125924,1710831125922',
};
case 'time-cp':
return {
value: '15:35:59',
newValue: '17:35:59',
};
case 'date-cp':
return {
value: '2024-07-09',
newValue: '2025-07-09',
};
case 'dateTime-cp':
return {
value: '2024-07-09 15:07:59',
newValue: '2025-07-09 15:07:59',
};
default:
return {
value: '',
@ -346,6 +409,30 @@ export const createCustomPropertyForEntity = async (
},
}
: {}),
...(item.name === 'time-cp'
? {
customPropertyConfig: {
config: 'HH:mm:ss',
},
}
: {}),
...(item.name === 'date-cp'
? {
customPropertyConfig: {
config: 'yyyy-MM-dd',
},
}
: {}),
...(item.name === 'dateTime-cp'
? {
customPropertyConfig: {
config: 'yyyy-MM-dd HH:mm:ss',
},
}
: {}),
},
}
);
@ -368,3 +455,229 @@ export const createCustomPropertyForEntity = async (
return { customProperties, cleanupUser };
};
export const addCustomPropertiesForEntity = async ({
page,
propertyName,
customPropertyData,
customType,
enumConfig,
formatConfig,
entityReferenceConfig,
}: {
page: Page;
propertyName: string;
customPropertyData: { description: string };
customType: string;
enumConfig?: { values: string[]; multiSelect: boolean };
formatConfig?: string;
entityReferenceConfig?: string[];
}) => {
// Add Custom property for selected entity
await page.click('[data-testid="add-field-button"]');
// Trigger validation
await page.click('[data-testid="create-button"]');
await expect(page.locator('#name_help')).toContainText('Name is required');
await expect(page.locator('#propertyType_help')).toContainText(
'Property Type is required'
);
await expect(page.locator('#description_help')).toContainText(
'Description is required'
);
// Validation checks
await page.fill(
'[data-testid="name"]',
CUSTOM_PROPERTY_INVALID_NAMES.CAPITAL_CASE
);
await expect(page.locator('#name_help')).toContainText(
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR
);
await page.fill(
'[data-testid="name"]',
CUSTOM_PROPERTY_INVALID_NAMES.WITH_UNDERSCORE
);
await expect(page.locator('#name_help')).toContainText(
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR
);
await page.fill(
'[data-testid="name"]',
CUSTOM_PROPERTY_INVALID_NAMES.WITH_SPACE
);
await expect(page.locator('#name_help')).toContainText(
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR
);
await page.fill(
'[data-testid="name"]',
CUSTOM_PROPERTY_INVALID_NAMES.WITH_DOTS
);
await expect(page.locator('#name_help')).toContainText(
CUSTOM_PROPERTY_NAME_VALIDATION_ERROR
);
// Name in another language
await page.fill('[data-testid="name"]', '汝らヴェディア');
await expect(page.locator('#name_help')).not.toBeVisible();
// Correct name
await page.fill('[data-testid="name"]', propertyName);
// Select custom type
await page.locator('[id="root\\/propertyType"]').fill(customType);
await page.getByTitle(`${customType}`, { exact: true }).click();
// Enum configuration
if (customType === 'Enum' && enumConfig) {
for (const val of enumConfig.values) {
await page.fill('#root\\/enumConfig', `${val}{Enter}`);
}
if (enumConfig.multiSelect) {
await page.click('#root\\/multiSelect');
}
}
// Entity reference configuration
if (
ENTITY_REFERENCE_PROPERTIES.includes(customType) &&
entityReferenceConfig
) {
for (const val of entityReferenceConfig) {
await page.click('#root\\/entityReferenceConfig');
await page.fill('#root\\/entityReferenceConfig', val);
await page.click(`[title="${val}"]`);
}
}
// Format configuration
if (['Date', 'Date Time', 'Time'].includes(customType)) {
await page.fill('#root\\/formatConfig', 'invalid-format');
await expect(page.locator('#formatConfig_help')).toContainText(
'Format is invalid'
);
if (formatConfig) {
await page.fill('#root\\/formatConfig', formatConfig);
}
}
// Description
await page.fill(descriptionBox, customPropertyData.description);
const createPropertyPromise = page.waitForResponse(
'/api/v1/metadata/types/name/*?fields=customProperties'
);
await page.click('[data-testid="create-button"]');
const response = await createPropertyPromise;
expect(response.status()).toBe(200);
await expect(
page.getByRole('row', { name: new RegExp(propertyName, 'i') })
).toBeVisible();
};
export const editCreatedProperty = async (
page: Page,
propertyName: string,
type?: string
) => {
// Fetching for edit button
const editButton = page.locator(
`[data-row-key="${propertyName}"] [data-testid="edit-button"]`
);
if (type === 'Enum') {
await expect(
page.locator(
`[data-row-key="${propertyName}"] [data-testid="enum-config"]`
)
).toContainText('["enum1","enum2","enum3"]');
}
await editButton.click();
await page.locator(descriptionBox).fill('');
await page.locator(descriptionBox).fill('This is new description');
if (type === 'Enum') {
await page
.locator('#root\\/customPropertyConfig')
.fill(`updatedValue{Enter}`);
await page.click('body'); // Equivalent to clicking outside
}
if (ENTITY_REFERENCE_PROPERTIES.includes(type ?? '')) {
await page.locator('#root\\/customPropertyConfig').click();
await page.locator('#root\\/customPropertyConfig').fill(`Table{Enter}`);
await page.click('body'); // Equivalent to clicking outside
}
const patchRequest = page.waitForResponse('/api/v1/metadata/types/*');
await page.locator('button[type="submit"]').click();
const response = await patchRequest;
expect(response.status()).toBe(200);
await expect(page.locator('.ant-modal-wrap')).not.toBeVisible();
// Fetching for updated descriptions for the created custom property
await expect(
page.locator(
`[data-row-key="${propertyName}"] [data-testid="viewer-container"]`
)
).toContainText('This is new description');
if (type === 'Enum') {
await expect(
page.locator(
`[data-row-key="${propertyName}"] [data-testid="enum-config"]`
)
).toContainText('["enum1","enum2","enum3","updatedValue"]');
}
if (ENTITY_REFERENCE_PROPERTIES.includes(type ?? '')) {
await expect(
page.locator(
`[data-row-key="${propertyName}"] [data-testid="${propertyName}-config"]`
)
).toContainText('["user","team","table"]');
}
};
export const deleteCreatedProperty = async (
page: Page,
propertyName: string
) => {
// Fetching for delete button
await page
.locator(`[data-row-key="${propertyName}"]`)
.scrollIntoViewIfNeeded();
await page
.locator(`[data-row-key="${propertyName}"] [data-testid="delete-button"]`)
.click();
// Checking property name is present on the delete pop-up
await expect(page.locator('[data-testid="body-text"]')).toContainText(
propertyName
);
// Ensure the save button is visible before clicking
await expect(page.locator('[data-testid="save-button"]')).toBeVisible();
await page.locator('[data-testid="save-button"]').click();
};

View File

@ -35,9 +35,35 @@ $$
$$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`.
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`.
$$
**Supported Date formats**
- `yyyy-MM-dd`
- `dd MMM yyyy`
- `MM/dd/yyyy`
- `dd/MM/yyyy`
- `dd-MM-yyyy`
- `yyyyDDD`
- `d MMMM yyyy`
**Supported DateTime formats**
- `MMM dd HH:mm:ss yyyy`
- `yyyy-MM-dd HH:mm:ss`
- `MM/dd/yyyy HH:mm:ss`
- `dd/MM/yyyy HH:mm:ss`
- `dd-MM-yyyy HH:mm:ss`
- `yyyy-MM-dd HH:mm:ss.SSS`
- `yyyy-MM-dd HH:mm:ss.SSSSSS`
- `dd MMMM yyyy HH:mm:ss`
**Supported Time formats**
- `HH:mm:ss`
$$section
### Entity Reference type $(id="entityReferenceConfig")

View File

@ -21,9 +21,9 @@ const mockGoBack = jest.fn();
const mockPropertyTypes = [
{
id: '153a0c07-6480-404e-990b-555a42c8a7b5',
name: 'date',
fullyQualifiedName: 'date',
displayName: 'date',
name: 'date-cp',
fullyQualifiedName: 'date-cp',
displayName: 'date-cp',
description: '"Date in ISO 8601 format in UTC. Example - \'2018-11-13\'."',
category: 'field',
nameSpace: 'basic',
@ -35,9 +35,9 @@ const mockPropertyTypes = [
},
{
id: '6ce245d8-80c0-4641-9b60-32cf03ca79a2',
name: 'dateTime',
fullyQualifiedName: 'dateTime',
displayName: 'dateTime',
name: 'dateTime-cp',
fullyQualifiedName: 'dateTime-cp',
displayName: 'dateTime-cp',
description:
'"Date and time in ISO 8601 format. Example - \'2018-11-13T20:20:39+00:00\'."',
category: 'field',

View File

@ -14,7 +14,7 @@
import { Button, Col, Form, Row } from 'antd';
import { AxiosError } from 'axios';
import { t } from 'i18next';
import { isUndefined, omit, omitBy, startCase } from 'lodash';
import { isUndefined, map, omit, omitBy, startCase } from 'lodash';
import React, {
FocusEvent,
useCallback,
@ -24,10 +24,10 @@ import React, {
} from 'react';
import { useHistory, useParams } from 'react-router-dom';
import {
DISABLED_PROPERTY_TYPES,
ENTITY_REFERENCE_OPTIONS,
PROPERTY_TYPES_WITH_ENTITY_REFERENCE,
PROPERTY_TYPES_WITH_FORMAT,
SUPPORTED_FORMAT_MAP,
} from '../../../../constants/CustomProperty.constants';
import { GlobalSettingsMenuCategory } from '../../../../constants/GlobalSettings.constants';
import { CUSTOM_PROPERTY_NAME_REGEX } from '../../../../constants/regex.constants';
@ -49,7 +49,6 @@ 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';
@ -58,8 +57,6 @@ import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels';
import ServiceDocPanel from '../../../common/ServiceDocPanel/ServiceDocPanel';
import TitleBreadcrumb from '../../../common/TitleBreadcrumb/TitleBreadcrumb.component';
type PropertyType = { key: string; label: string; value: string | undefined };
const AddCustomProperty = () => {
const [form] = Form.useForm();
const { entityType } = useParams<{ entityType: EntityType }>();
@ -97,43 +94,40 @@ const AddCustomProperty = () => {
);
const propertyTypeOptions = useMemo(() => {
return propertyTypes.reduce((acc: PropertyType[], type) => {
if (DISABLED_PROPERTY_TYPES.includes(type.name)) {
return acc;
}
return [
...acc,
{
key: type.name,
label: startCase(type.displayName ?? type.name),
value: type.id,
},
];
}, []);
return map(propertyTypes, (type) => ({
key: type.name,
// Remove -cp from the name and convert to start case
label: startCase((type.displayName ?? type.name).replace(/-cp/g, '')),
value: type.id,
}));
}, [propertyTypes]);
const { hasEnumConfig, hasFormatConfig, hasEntityReferenceConfig } =
useMemo(() => {
const watchedOption = propertyTypeOptions.find(
(option) => option.value === watchedPropertyType
);
const watchedOptionKey = watchedOption?.key ?? '';
const {
hasEnumConfig,
hasFormatConfig,
hasEntityReferenceConfig,
watchedOption,
} = useMemo(() => {
const watchedOption = propertyTypeOptions.find(
(option) => option.value === watchedPropertyType
);
const watchedOptionKey = watchedOption?.key ?? '';
const hasEnumConfig = watchedOptionKey === 'enum';
const hasEnumConfig = watchedOptionKey === 'enum';
const hasFormatConfig =
PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey);
const hasFormatConfig =
PROPERTY_TYPES_WITH_FORMAT.includes(watchedOptionKey);
const hasEntityReferenceConfig =
PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(watchedOptionKey);
const hasEntityReferenceConfig =
PROPERTY_TYPES_WITH_ENTITY_REFERENCE.includes(watchedOptionKey);
return {
hasEnumConfig,
hasFormatConfig,
hasEntityReferenceConfig,
};
}, [watchedPropertyType, propertyTypeOptions]);
return {
hasEnumConfig,
hasFormatConfig,
hasEntityReferenceConfig,
watchedOption,
};
}, [watchedPropertyType, propertyTypeOptions]);
const fetchPropertyType = async () => {
try {
@ -337,7 +331,13 @@ const AddCustomProperty = () => {
rules: [
{
validator: (_, value) => {
if (!isValidDateFormat(value)) {
const propertyName = watchedOption?.key ?? '';
const supportedFormats =
SUPPORTED_FORMAT_MAP[
propertyName as keyof typeof SUPPORTED_FORMAT_MAP
];
if (!supportedFormats.includes(value)) {
return Promise.reject(
t('label.field-invalid', {
field: t('label.format'),

View File

@ -160,7 +160,7 @@ describe('Test PropertyValue Component', () => {
const extension = { yNumber: '20-03-2024' };
const propertyType = {
...mockData.property.propertyType,
name: 'date',
name: 'date-cp',
};
render(
<PropertyValue
@ -187,7 +187,7 @@ describe('Test PropertyValue Component', () => {
};
const propertyType = {
...mockData.property.propertyType,
name: 'dateTime',
name: 'dateTime-cp',
};
render(
<PropertyValue
@ -216,7 +216,7 @@ describe('Test PropertyValue Component', () => {
};
const propertyType = {
...mockData.property.propertyType,
name: 'time',
name: 'time-cp',
};
render(
<PropertyValue

View File

@ -225,12 +225,12 @@ export const PropertyValue: FC<PropertyValueProps> = ({
);
}
case 'date':
case 'dateTime': {
case 'date-cp':
case 'dateTime-cp': {
// Default format is 'yyyy-mm-dd'
const format =
toUpper(property.customPropertyConfig?.config as string) ??
'yyyy-mm-dd';
const format = toUpper(
(property.customPropertyConfig?.config as string) ?? 'yyyy-mm-dd'
);
const initialValues = {
dateTimeValue: value ? moment(value, format) : undefined,
@ -263,7 +263,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
data-testid="date-time-picker"
disabled={isLoading}
format={format}
showTime={propertyType.name === 'dateTime'}
showTime={propertyType.name === 'dateTime-cp'}
style={{ width: '250px' }}
/>
</Form.Item>
@ -272,8 +272,9 @@ export const PropertyValue: FC<PropertyValueProps> = ({
);
}
case 'time': {
const format = 'HH:mm:ss';
case 'time-cp': {
const format =
(property.customPropertyConfig?.config as string) ?? 'HH:mm:ss';
const initialValues = {
time: value ? moment(value, format) : undefined,
};
@ -303,6 +304,7 @@ export const PropertyValue: FC<PropertyValueProps> = ({
allowClear
data-testid="time-picker"
disabled={isLoading}
format={format}
style={{ width: '250px' }}
/>
</Form.Item>
@ -788,9 +790,9 @@ export const PropertyValue: FC<PropertyValueProps> = ({
case 'string':
case 'integer':
case 'number':
case 'date':
case 'dateTime':
case 'time':
case 'date-cp':
case 'dateTime-cp':
case 'time-cp':
case 'email':
case 'timestamp':
case 'duration':

View File

@ -10,14 +10,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const PROPERTY_TYPES_WITH_FORMAT = ['date', 'dateTime'];
export const DISABLED_PROPERTY_TYPES = [
'time',
'dateTime',
'timeInterval',
'date',
];
export const PROPERTY_TYPES_WITH_FORMAT = ['date-cp', 'dateTime-cp', 'time-cp'];
export const PROPERTY_TYPES_WITH_ENTITY_REFERENCE = [
'entityReference',
@ -101,3 +94,35 @@ export const ENTITY_REFERENCE_OPTIONS = [
label: 'Team',
},
];
// supported date formats on backend
export const SUPPORTED_DATE_FORMATS = [
'yyyy-MM-dd',
'dd MMM yyyy',
'MM/dd/yyyy',
'dd/MM/yyyy',
'dd-MM-yyyy',
'yyyyDDD',
'd MMMM yyyy',
];
// supported date time formats on backend
export const SUPPORTED_DATE_TIME_FORMATS = [
'MMM dd HH:mm:ss yyyy',
'yyyy-MM-dd HH:mm:ss',
'MM/dd/yyyy HH:mm:ss',
'dd/MM/yyyy HH:mm:ss',
'dd-MM-yyyy HH:mm:ss',
'yyyy-MM-dd HH:mm:ss.SSS',
'yyyy-MM-dd HH:mm:ss.SSSSSS',
'dd MMMM yyyy HH:mm:ss',
];
// supported time formats on backend
export const SUPPORTED_TIME_FORMATS = ['HH:mm:ss'];
export const SUPPORTED_FORMAT_MAP = {
'date-cp': SUPPORTED_DATE_FORMATS,
'dateTime-cp': SUPPORTED_DATE_TIME_FORMATS,
'time-cp': SUPPORTED_TIME_FORMATS,
};