mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-05 03:54:23 +00:00
Feat(ui): add and edit certifications from asset page (#21758)
* Feat(ui): add and edit certifications from asset page (#21344) * added styling to certification * added tests * changed icons for certificatie popup --------- Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> * #19529 Improvise import to include certification for Assets (Database, DatabaseSchema, DatabaseService), Introduce EDIT_CERTIFICATION policy and Fix Elastic Search Indexes on Certification (#21564) * #19529 - Adding certification field in json schema for databaseService.json, databaseServiceCsvDocumentation.json for bulk import facility, Implemented bulk import for non-recursive databaseService import async * #19529 -Bulk async import - databaseSchema entity * #19529 -Bulk async import - database entity * #19529 - Initial Implementation of Search Indexes for Certificate during bulk Import * #19529 - Edit Certification Policy Implementation, Attaching it to DataStewards and DataConsumers, also adding it to Migration * #19529 - Updated ElasticSearch Index mapping name * #19529 - Updated the CSV indices for recursive import * #19529 - Single Test working as expected but not as a test suite. * #19529 - Fixed DatabaseSchemaResourceTest * #19529 - Fixed DatabaseResource Test for exportImportCSV * #19529 - Fixed DatabaseServiceResource Test for exportImportCSV * #19529 - Updated and Improvised ElasticSearch Certification functionality * #19529 - Added postgres migration as well along with mysql migration * #19529 - Removed migration scripts from v160 and placed it at v180, Included mappings for database_service in zh and jp, Applied mvn spotless:apply * #19529 - Adding certification field in json schema for databaseService.json, databaseServiceCsvDocumentation.json for bulk import facility, Implemented bulk import for non-recursive databaseService import async * #19529 -Bulk async import - databaseSchema entity * #19529 -Bulk async import - database entity * #19529 - Initial Implementation of Search Indexes for Certificate during bulk Import * #19529 - Edit Certification Policy Implementation, Attaching it to DataStewards and DataConsumers, also adding it to Migration * #19529 - Updated ElasticSearch Index mapping name * #19529 - Updated the CSV indices for recursive import * #19529 - Single Test working as expected but not as a test suite. * #19529 - Fixed DatabaseSchemaResourceTest * #19529 - Fixed DatabaseResource Test for exportImportCSV * #19529 - Fixed DatabaseServiceResource Test for exportImportCSV * #19529 - Updated and Improvised ElasticSearch Certification functionality * #19529 - Added postgres migration as well along with mysql migration * #19529 - Removed migration scripts from v160 and placed it at v180, Included mappings for database_service in zh and jp, Applied mvn spotless:apply * Applied mvn spotless:apply * Reused the Existing UPDATE_CERTIFICATION_SCRIPT for ElasticSearch Indexing * Added field certification in the static String FIELDS * fix playwright test around bulk action * #19529 - Persisting Null or empty in the bulk import for certification * #19529 - Persisting Null or empty in the bulk import for certification - Moved the if block to the top * mvn spotless:apply * Reverted an unimportant file * mvn spotless:apply * #19529 - Persisting the field Certification in clearFIeldsInternal * typescript files for edit_certification * Revert "typescript files for edit_certification" This reverts commit f5e5514a98008cbd0b62d7cb21fefe61659e97cb. * typescript files for edit_certification * mvn:spotless:apply * Removed correction * needed typescript file for edit_certification * Removed Unnecessary Comments * Improved Test Cases - Added DATA_ASSET_SEARCH alias instead of GLOBAL_ALIAS * Fixed csv values in order --------- Co-authored-by: Ashish Gupta <ashish@getcollate.io> Co-authored-by: System Administrator <root@192.168.1.4> Co-authored-by: sonika-shah <58761340+sonika-shah@users.noreply.github.com> Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> * update certification component * Fixed Certification Indexes * Fixed Missing Certification Label for DBSchema * add certification changes * fix test errors * Remove Certification field and mapping from database service asset * Removed certification from schema and fixed test * fix tests --------- Co-authored-by: Dhruv Parmar <83108871+dhruvjsx@users.noreply.github.com> Co-authored-by: Ram Narayan Balaji <81347100+yan-3005@users.noreply.github.com> Co-authored-by: Ashish Gupta <ashish@getcollate.io> Co-authored-by: System Administrator <root@192.168.1.4> Co-authored-by: sonika-shah <58761340+sonika-shah@users.noreply.github.com> Co-authored-by: Ram Narayan Balaji <ramnarayanb3005@gmail.com>
This commit is contained in:
parent
961a5357dd
commit
2689676b9a
@ -75,6 +75,7 @@ import org.openmetadata.schema.entity.data.DatabaseSchema;
|
||||
import org.openmetadata.schema.entity.data.StoredProcedure;
|
||||
import org.openmetadata.schema.entity.data.Table;
|
||||
import org.openmetadata.schema.type.ApiStatus;
|
||||
import org.openmetadata.schema.type.AssetCertification;
|
||||
import org.openmetadata.schema.type.ChangeEvent;
|
||||
import org.openmetadata.schema.type.Column;
|
||||
import org.openmetadata.schema.type.ColumnDataType;
|
||||
@ -378,6 +379,19 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
return tagLabels;
|
||||
}
|
||||
|
||||
protected AssetCertification getCertificationLabels(String certificationTag) {
|
||||
if (nullOrEmpty(certificationTag)) {
|
||||
return null;
|
||||
}
|
||||
TagLabel certificationLabel =
|
||||
new TagLabel().withTagFQN(certificationTag).withSource(TagLabel.TagSource.CLASSIFICATION);
|
||||
|
||||
return new AssetCertification()
|
||||
.withTagLabel(certificationLabel)
|
||||
.withAppliedDate(System.currentTimeMillis())
|
||||
.withExpiryDate(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public Map<String, Object> getExtension(CSVPrinter printer, CSVRecord csvRecord, int fieldNumber)
|
||||
throws IOException {
|
||||
String extensionString = csvRecord.get(fieldNumber);
|
||||
@ -1077,6 +1091,8 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
Pair.of(4, TagSource.CLASSIFICATION),
|
||||
Pair.of(5, TagSource.GLOSSARY),
|
||||
Pair.of(6, TagSource.CLASSIFICATION)));
|
||||
AssetCertification certification = getCertificationLabels(csvRecord.get(7));
|
||||
|
||||
schema
|
||||
.withId(UUID.randomUUID())
|
||||
.withName(csvRecord.get(0))
|
||||
@ -1085,10 +1101,11 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
.withDescription(csvRecord.get(2))
|
||||
.withOwners(getOwners(printer, csvRecord, 3))
|
||||
.withTags(tagLabels)
|
||||
.withRetentionPeriod(csvRecord.get(7))
|
||||
.withSourceUrl(csvRecord.get(8))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 9, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 10))
|
||||
.withCertification(certification)
|
||||
.withRetentionPeriod(csvRecord.get(8))
|
||||
.withSourceUrl(csvRecord.get(9))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 10, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 11))
|
||||
.withUpdatedAt(System.currentTimeMillis())
|
||||
.withUpdatedBy(importedBy);
|
||||
if (processRecord) {
|
||||
@ -1155,6 +1172,7 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
Pair.of(4, TagSource.CLASSIFICATION),
|
||||
Pair.of(5, TagSource.GLOSSARY),
|
||||
Pair.of(6, TagSource.CLASSIFICATION)));
|
||||
AssetCertification certification = getCertificationLabels(csvRecord.get(7));
|
||||
|
||||
// Populate table attributes
|
||||
table
|
||||
@ -1162,10 +1180,11 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
.withDescription(csvRecord.get(2))
|
||||
.withOwners(getOwners(printer, csvRecord, 3))
|
||||
.withTags(tagLabels)
|
||||
.withRetentionPeriod(csvRecord.get(7))
|
||||
.withSourceUrl(csvRecord.get(8))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 9, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 10))
|
||||
.withCertification(certification)
|
||||
.withRetentionPeriod(csvRecord.get(8))
|
||||
.withSourceUrl(csvRecord.get(9))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 10, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 11))
|
||||
.withUpdatedAt(System.currentTimeMillis())
|
||||
.withUpdatedBy(importedBy);
|
||||
if (processRecord) {
|
||||
@ -1228,8 +1247,8 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
Pair.of(4, TagSource.CLASSIFICATION),
|
||||
Pair.of(5, TagSource.GLOSSARY),
|
||||
Pair.of(6, TagSource.CLASSIFICATION)));
|
||||
|
||||
String languageStr = csvRecord.get(18);
|
||||
AssetCertification certification = getCertificationLabels(csvRecord.get(7));
|
||||
String languageStr = csvRecord.get(19);
|
||||
StoredProcedureLanguage language = null;
|
||||
|
||||
if (languageStr != null && !languageStr.isEmpty()) {
|
||||
@ -1241,16 +1260,17 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
}
|
||||
|
||||
StoredProcedureCode storedProcedureCode =
|
||||
new StoredProcedureCode().withCode(csvRecord.get(17)).withLanguage(language);
|
||||
new StoredProcedureCode().withCode(csvRecord.get(18)).withLanguage(language);
|
||||
|
||||
sp.withDisplayName(csvRecord.get(1))
|
||||
.withDescription(csvRecord.get(2))
|
||||
.withOwners(getOwners(printer, csvRecord, 3))
|
||||
.withTags(tagLabels)
|
||||
.withSourceUrl(csvRecord.get(8))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 9, Entity.DOMAIN))
|
||||
.withCertification(certification)
|
||||
.withSourceUrl(csvRecord.get(9))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 10, Entity.DOMAIN))
|
||||
.withStoredProcedureCode(storedProcedureCode)
|
||||
.withExtension(getExtension(printer, csvRecord, 10));
|
||||
.withExtension(getExtension(printer, csvRecord, 11));
|
||||
|
||||
if (processRecord) {
|
||||
// Only create the stored procedure if the schema actually exists
|
||||
@ -1318,7 +1338,7 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
private void updateColumnsFromCsvRecursive(Table table, CSVRecord csvRecord, CSVPrinter printer)
|
||||
throws IOException {
|
||||
String columnFqn = csvRecord.get(0);
|
||||
String columnFullyQualifiedName = csvRecord.get(12);
|
||||
String columnFullyQualifiedName = csvRecord.get(13);
|
||||
Column column = null;
|
||||
boolean columnExists = false;
|
||||
try {
|
||||
@ -1338,8 +1358,8 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
|
||||
column.withDisplayName(csvRecord.get(1));
|
||||
column.withDescription(csvRecord.get(2));
|
||||
column.withDataTypeDisplay(csvRecord.get(13));
|
||||
String dataTypeStr = csvRecord.get(14);
|
||||
column.withDataTypeDisplay(csvRecord.get(14));
|
||||
String dataTypeStr = csvRecord.get(15);
|
||||
if (nullOrEmpty(dataTypeStr)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Column dataType is mandatory for column: " + csvRecord.get(0));
|
||||
@ -1353,11 +1373,11 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
}
|
||||
|
||||
if (column.getDataType() == ColumnDataType.ARRAY) {
|
||||
if (nullOrEmpty(csvRecord.get(15))) {
|
||||
if (nullOrEmpty(csvRecord.get(16))) {
|
||||
throw new IllegalArgumentException(
|
||||
"Array data type is mandatory for ARRAY columns: " + csvRecord.get(0));
|
||||
}
|
||||
column.withArrayDataType(ColumnDataType.fromValue(csvRecord.get(15)));
|
||||
column.withArrayDataType(ColumnDataType.fromValue(csvRecord.get(16)));
|
||||
}
|
||||
|
||||
if (column.getDataType() == ColumnDataType.STRUCT && column.getChildren() == null) {
|
||||
@ -1365,7 +1385,7 @@ public abstract class EntityCsv<T extends EntityInterface> {
|
||||
}
|
||||
|
||||
column.withDataLength(
|
||||
parseDataLength(csvRecord.get(16), column.getDataType(), column.getName()));
|
||||
parseDataLength(csvRecord.get(17), column.getDataType(), column.getName()));
|
||||
|
||||
List<TagLabel> tagLabels =
|
||||
getTagLabels(
|
||||
|
||||
@ -50,6 +50,7 @@ public class ResourceRegistry {
|
||||
mapFieldOperation(MetadataOperation.EDIT_TEAMS, "teams");
|
||||
mapFieldOperation(MetadataOperation.EDIT_DESCRIPTION, Entity.FIELD_DESCRIPTION);
|
||||
mapFieldOperation(MetadataOperation.EDIT_DISPLAY_NAME, Entity.FIELD_DISPLAY_NAME);
|
||||
mapFieldOperation(MetadataOperation.EDIT_CERTIFICATION, Entity.FIELD_CERTIFICATION);
|
||||
|
||||
// Set up "all" resource descriptor that includes operations for all entities
|
||||
List<MetadataOperation> allOperations = Arrays.asList(MetadataOperation.values());
|
||||
@ -98,6 +99,9 @@ public class ResourceRegistry {
|
||||
if (entityFields.contains("reviewers")) {
|
||||
operations.add(MetadataOperation.EDIT_REVIEWERS);
|
||||
}
|
||||
if (entityFields.contains(Entity.FIELD_CERTIFICATION)) {
|
||||
operations.add(MetadataOperation.EDIT_CERTIFICATION);
|
||||
}
|
||||
return new ArrayList<>(operations);
|
||||
}
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ import org.openmetadata.schema.entity.data.DatabaseSchema;
|
||||
import org.openmetadata.schema.entity.data.StoredProcedure;
|
||||
import org.openmetadata.schema.entity.data.Table;
|
||||
import org.openmetadata.schema.entity.services.DatabaseService;
|
||||
import org.openmetadata.schema.type.AssetCertification;
|
||||
import org.openmetadata.schema.type.DatabaseProfilerConfig;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.Include;
|
||||
@ -351,6 +352,11 @@ public class DatabaseRepository extends EntityRepository<Database> {
|
||||
addTagLabels(recordList, entity.getTags());
|
||||
addGlossaryTerms(recordList, entity.getTags());
|
||||
addTagTiers(recordList, entity.getTags());
|
||||
addField(
|
||||
recordList,
|
||||
entity.getCertification() != null && entity.getCertification().getTagLabel() != null
|
||||
? entity.getCertification().getTagLabel().getTagFQN()
|
||||
: "");
|
||||
Object retentionPeriod = EntityUtil.getEntityField(entity, "retentionPeriod");
|
||||
Object sourceUrl = EntityUtil.getEntityField(entity, "sourceUrl");
|
||||
addField(recordList, retentionPeriod == null ? "" : retentionPeriod.toString());
|
||||
@ -411,9 +417,9 @@ public class DatabaseRepository extends EntityRepository<Database> {
|
||||
}
|
||||
|
||||
// Get entityType and fullyQualifiedName if provided
|
||||
String entityType = csvRecord.size() > 11 ? csvRecord.get(11) : DATABASE_SCHEMA;
|
||||
String entityType = csvRecord.size() > 12 ? csvRecord.get(12) : DATABASE_SCHEMA;
|
||||
String entityFQN =
|
||||
csvRecord.size() > 12 ? StringEscapeUtils.unescapeCsv(csvRecord.get(12)) : null;
|
||||
csvRecord.size() > 13 ? StringEscapeUtils.unescapeCsv(csvRecord.get(13)) : null;
|
||||
|
||||
if (DATABASE_SCHEMA.equals(entityType)) {
|
||||
createSchemaEntity(printer, csvRecord, entityFQN);
|
||||
@ -443,7 +449,8 @@ public class DatabaseRepository extends EntityRepository<Database> {
|
||||
.withService(database.getService());
|
||||
}
|
||||
|
||||
// Headers: name, displayName, description, owner, tags, glossaryTerms, tiers retentionPeriod,
|
||||
// Headers: name, displayName, description, owner, tags, glossaryTerms, tiers, certification,
|
||||
// retentionPeriod,
|
||||
// sourceUrl, domain
|
||||
// Field 1,2,3,6,7 - database schema name, displayName, description
|
||||
List<TagLabel> tagLabels =
|
||||
@ -454,6 +461,9 @@ public class DatabaseRepository extends EntityRepository<Database> {
|
||||
Pair.of(4, TagLabel.TagSource.CLASSIFICATION),
|
||||
Pair.of(5, TagLabel.TagSource.GLOSSARY),
|
||||
Pair.of(6, TagLabel.TagSource.CLASSIFICATION)));
|
||||
|
||||
AssetCertification certification = getCertificationLabels(csvRecord.get(7));
|
||||
|
||||
schema
|
||||
.withName(csvRecord.get(0))
|
||||
.withFullyQualifiedName(schemaFqn)
|
||||
@ -461,10 +471,11 @@ public class DatabaseRepository extends EntityRepository<Database> {
|
||||
.withDescription(csvRecord.get(2))
|
||||
.withOwners(getOwners(printer, csvRecord, 3))
|
||||
.withTags(tagLabels)
|
||||
.withRetentionPeriod(csvRecord.get(7))
|
||||
.withSourceUrl(csvRecord.get(8))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 9, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 10));
|
||||
.withCertification(certification)
|
||||
.withRetentionPeriod(csvRecord.get(8))
|
||||
.withSourceUrl(csvRecord.get(9))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 10, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 11));
|
||||
if (processRecord) {
|
||||
createEntity(printer, csvRecord, schema, DATABASE_SCHEMA);
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ import org.openmetadata.schema.entity.data.Database;
|
||||
import org.openmetadata.schema.entity.data.DatabaseSchema;
|
||||
import org.openmetadata.schema.entity.data.StoredProcedure;
|
||||
import org.openmetadata.schema.entity.data.Table;
|
||||
import org.openmetadata.schema.type.AssetCertification;
|
||||
import org.openmetadata.schema.type.DatabaseSchemaProfilerConfig;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.Include;
|
||||
@ -368,6 +369,11 @@ public class DatabaseSchemaRepository extends EntityRepository<DatabaseSchema> {
|
||||
addTagLabels(recordList, entity.getTags());
|
||||
addGlossaryTerms(recordList, entity.getTags());
|
||||
addTagTiers(recordList, entity.getTags());
|
||||
addField(
|
||||
recordList,
|
||||
entity.getCertification() != null && entity.getCertification().getTagLabel() != null
|
||||
? entity.getCertification().getTagLabel().getTagFQN()
|
||||
: "");
|
||||
Object retentionPeriod = EntityUtil.getEntityField(entity, "retentionPeriod");
|
||||
Object sourceUrl = EntityUtil.getEntityField(entity, "sourceUrl");
|
||||
addField(recordList, retentionPeriod == null ? "" : retentionPeriod.toString());
|
||||
@ -433,7 +439,7 @@ public class DatabaseSchemaRepository extends EntityRepository<DatabaseSchema> {
|
||||
.withDatabaseSchema(schema.getEntityReference());
|
||||
}
|
||||
|
||||
// Headers: name, displayName, description, owners, tags, glossaryTerms, tiers
|
||||
// Headers: name, displayName, description, owners, tags, glossaryTerms, tiers, certification,
|
||||
// retentionPeriod,
|
||||
// sourceUrl, domain
|
||||
// Field 1,2,3,6,7 - database schema name, displayName, description
|
||||
@ -445,6 +451,9 @@ public class DatabaseSchemaRepository extends EntityRepository<DatabaseSchema> {
|
||||
Pair.of(4, TagLabel.TagSource.CLASSIFICATION),
|
||||
Pair.of(5, TagLabel.TagSource.GLOSSARY),
|
||||
Pair.of(6, TagLabel.TagSource.CLASSIFICATION)));
|
||||
|
||||
AssetCertification certification = getCertificationLabels(csvRecord.get(7));
|
||||
|
||||
table
|
||||
.withName(csvRecord.get(0))
|
||||
.withFullyQualifiedName(tableFqn)
|
||||
@ -452,11 +461,12 @@ public class DatabaseSchemaRepository extends EntityRepository<DatabaseSchema> {
|
||||
.withDescription(csvRecord.get(2))
|
||||
.withOwners(getOwners(printer, csvRecord, 3))
|
||||
.withTags(tagLabels)
|
||||
.withRetentionPeriod(csvRecord.get(7))
|
||||
.withSourceUrl(csvRecord.get(8))
|
||||
.withCertification(certification)
|
||||
.withRetentionPeriod(csvRecord.get(8))
|
||||
.withSourceUrl(csvRecord.get(9))
|
||||
.withColumns(nullOrEmpty(table.getColumns()) ? new ArrayList<>() : table.getColumns())
|
||||
.withDomain(getEntityReference(printer, csvRecord, 9, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 10));
|
||||
.withDomain(getEntityReference(printer, csvRecord, 10, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 11));
|
||||
|
||||
if (processRecord) {
|
||||
createEntity(printer, csvRecord, table, TABLE);
|
||||
@ -468,8 +478,8 @@ public class DatabaseSchemaRepository extends EntityRepository<DatabaseSchema> {
|
||||
CSVRecord csvRecord = getNextRecord(printer, csvRecords);
|
||||
|
||||
// Get entityType and fullyQualifiedName if provided
|
||||
String entityType = csvRecord.size() > 11 ? csvRecord.get(11) : TABLE;
|
||||
String entityFQN = csvRecord.size() > 12 ? csvRecord.get(12) : null;
|
||||
String entityType = csvRecord.size() > 12 ? csvRecord.get(12) : TABLE;
|
||||
String entityFQN = csvRecord.size() > 13 ? csvRecord.get(13) : null;
|
||||
|
||||
if (TABLE.equals(entityType)) {
|
||||
createTableEntity(printer, csvRecord, entityFQN);
|
||||
|
||||
@ -44,6 +44,7 @@ import org.openmetadata.schema.entity.data.Database;
|
||||
import org.openmetadata.schema.entity.data.DatabaseSchema;
|
||||
import org.openmetadata.schema.entity.services.DatabaseService;
|
||||
import org.openmetadata.schema.entity.services.ServiceType;
|
||||
import org.openmetadata.schema.type.AssetCertification;
|
||||
import org.openmetadata.schema.type.Include;
|
||||
import org.openmetadata.schema.type.TagLabel;
|
||||
import org.openmetadata.schema.type.csv.CsvDocumentation;
|
||||
@ -165,6 +166,11 @@ public class DatabaseServiceRepository
|
||||
addTagLabels(recordList, entity.getTags());
|
||||
addGlossaryTerms(recordList, entity.getTags());
|
||||
addTagTiers(recordList, entity.getTags());
|
||||
addField(
|
||||
recordList,
|
||||
entity.getCertification() != null && entity.getCertification().getTagLabel() != null
|
||||
? entity.getCertification().getTagLabel().getTagFQN()
|
||||
: "");
|
||||
|
||||
if (recursive) {
|
||||
Object retentionPeriod = EntityUtil.getEntityField(entity, "retentionPeriod");
|
||||
@ -226,8 +232,8 @@ public class DatabaseServiceRepository
|
||||
database = new Database().withService(service.getEntityReference());
|
||||
}
|
||||
|
||||
// Headers: name, displayName, description, owners, tags, glossaryTerms, tiers, domain
|
||||
// Field 1,2,3,6,7 - database service name, displayName, description
|
||||
// Headers: name, displayName, description, owners, tags, glossaryTerms, tiers, certification,
|
||||
// domain, extension
|
||||
List<TagLabel> tagLabels =
|
||||
getTagLabels(
|
||||
printer,
|
||||
@ -236,6 +242,9 @@ public class DatabaseServiceRepository
|
||||
Pair.of(4, TagLabel.TagSource.CLASSIFICATION),
|
||||
Pair.of(5, TagLabel.TagSource.GLOSSARY),
|
||||
Pair.of(6, TagLabel.TagSource.CLASSIFICATION)));
|
||||
|
||||
AssetCertification certification = getCertificationLabels(csvRecord.get(7));
|
||||
|
||||
database
|
||||
.withName(csvRecord.get(0))
|
||||
.withFullyQualifiedName(databaseFqn)
|
||||
@ -243,8 +252,9 @@ public class DatabaseServiceRepository
|
||||
.withDescription(csvRecord.get(2))
|
||||
.withOwners(getOwners(printer, csvRecord, 3))
|
||||
.withTags(tagLabels)
|
||||
.withDomain(getEntityReference(printer, csvRecord, 7, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 8));
|
||||
.withCertification(certification)
|
||||
.withDomain(getEntityReference(printer, csvRecord, 8, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 9));
|
||||
|
||||
if (processRecord) {
|
||||
createEntity(printer, csvRecord, database, DATABASE);
|
||||
@ -256,8 +266,8 @@ public class DatabaseServiceRepository
|
||||
CSVRecord csvRecord = getNextRecord(printer, csvRecords);
|
||||
|
||||
// Get entityType and fullyQualifiedName if provided
|
||||
String entityType = csvRecord.size() > 11 ? csvRecord.get(11) : DATABASE;
|
||||
String entityFQN = csvRecord.size() > 12 ? csvRecord.get(12) : null;
|
||||
String entityType = csvRecord.size() > 12 ? csvRecord.get(12) : DATABASE;
|
||||
String entityFQN = csvRecord.size() > 13 ? csvRecord.get(13) : null;
|
||||
|
||||
if (DATABASE.equals(entityType)) {
|
||||
createDatabaseEntity(printer, csvRecord, entityFQN);
|
||||
@ -298,15 +308,17 @@ public class DatabaseServiceRepository
|
||||
Pair.of(4, TagLabel.TagSource.CLASSIFICATION),
|
||||
Pair.of(5, TagLabel.TagSource.GLOSSARY),
|
||||
Pair.of(6, TagLabel.TagSource.CLASSIFICATION)));
|
||||
AssetCertification certification = getCertificationLabels(csvRecord.get(7));
|
||||
database
|
||||
.withName(csvRecord.get(0))
|
||||
.withDisplayName(csvRecord.get(1))
|
||||
.withDescription(csvRecord.get(2))
|
||||
.withOwners(getOwners(printer, csvRecord, 3))
|
||||
.withTags(tagLabels)
|
||||
.withSourceUrl(csvRecord.get(8))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 9, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 10));
|
||||
.withCertification(certification)
|
||||
.withSourceUrl(csvRecord.get(9))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 10, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 11));
|
||||
|
||||
if (processRecord) {
|
||||
createEntity(printer, csvRecord, database, DATABASE);
|
||||
@ -364,6 +376,7 @@ public class DatabaseServiceRepository
|
||||
Pair.of(4, TagLabel.TagSource.CLASSIFICATION),
|
||||
Pair.of(5, TagLabel.TagSource.GLOSSARY),
|
||||
Pair.of(6, TagLabel.TagSource.CLASSIFICATION)));
|
||||
AssetCertification certification = getCertificationLabels(csvRecord.get(7));
|
||||
schema
|
||||
.withId(UUID.randomUUID())
|
||||
.withName(csvRecord.get(0))
|
||||
@ -372,10 +385,11 @@ public class DatabaseServiceRepository
|
||||
.withDescription(csvRecord.get(2))
|
||||
.withOwners(getOwners(printer, csvRecord, 3))
|
||||
.withTags(tagLabels)
|
||||
.withRetentionPeriod(csvRecord.get(7))
|
||||
.withSourceUrl(csvRecord.get(8))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 9, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 10))
|
||||
.withCertification(certification)
|
||||
.withRetentionPeriod(csvRecord.get(8))
|
||||
.withSourceUrl(csvRecord.get(9))
|
||||
.withDomain(getEntityReference(printer, csvRecord, 10, Entity.DOMAIN))
|
||||
.withExtension(getExtension(printer, csvRecord, 11))
|
||||
.withUpdatedAt(System.currentTimeMillis())
|
||||
.withUpdatedBy(importedBy);
|
||||
if (processRecord) {
|
||||
|
||||
@ -1057,7 +1057,10 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
||||
public final T setFieldsInternal(T entity, Fields fields) {
|
||||
entity.setOwners(fields.contains(FIELD_OWNERS) ? getOwners(entity) : entity.getOwners());
|
||||
entity.setTags(fields.contains(FIELD_TAGS) ? getTags(entity) : entity.getTags());
|
||||
entity.setCertification(fields.contains(FIELD_TAGS) ? getCertification(entity) : null);
|
||||
entity.setCertification(
|
||||
fields.contains(FIELD_TAGS) || fields.contains(FIELD_CERTIFICATION)
|
||||
? getCertification(entity)
|
||||
: null);
|
||||
entity.setExtension(
|
||||
fields.contains(FIELD_EXTENSION) ? getExtension(entity) : entity.getExtension());
|
||||
// Always return domains of entity
|
||||
@ -3698,8 +3701,21 @@ public abstract class EntityRepository<T extends EntityInterface> {
|
||||
AssetCertification origCertification = original.getCertification();
|
||||
AssetCertification updatedCertification = updated.getCertification();
|
||||
|
||||
if (Objects.equals(origCertification, updatedCertification) || updatedCertification == null)
|
||||
LOG.debug(
|
||||
"Updating certification - Original: {}, Updated: {}",
|
||||
origCertification,
|
||||
updatedCertification);
|
||||
|
||||
if (updatedCertification == null) {
|
||||
LOG.debug("Setting certification to null");
|
||||
recordChange(FIELD_CERTIFICATION, origCertification, updatedCertification, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Objects.equals(origCertification, updatedCertification)) {
|
||||
LOG.debug("Certification unchanged");
|
||||
return;
|
||||
}
|
||||
|
||||
SystemRepository systemRepository = Entity.getSystemRepository();
|
||||
AssetCertificationSettings assetCertificationSettings =
|
||||
|
||||
@ -863,6 +863,7 @@ public class TableRepository extends EntityRepository<Table> {
|
||||
|
||||
// More fields that should be empty for columns
|
||||
addField(recordList, ""); // tiers (empty for columns)
|
||||
addField(recordList, ""); // certification (empty for columns)
|
||||
addField(recordList, ""); // retentionPeriod (empty for columns)
|
||||
addField(recordList, ""); // sourceUrl (empty for columns)
|
||||
addField(recordList, ""); // domain (empty for columns)
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
package org.openmetadata.service.migration.mysql.v180;
|
||||
|
||||
import static org.openmetadata.service.migration.utils.v180.MigrationUtil.addCertificationOperationsToPolicy;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.openmetadata.service.migration.api.MigrationProcessImpl;
|
||||
import org.openmetadata.service.migration.utils.MigrationFile;
|
||||
|
||||
public class Migration extends MigrationProcessImpl {
|
||||
|
||||
public Migration(MigrationFile migrationFile) {
|
||||
super(migrationFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void runDataMigration() {
|
||||
addCertificationOperationsToPolicy(collectionDAO);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package org.openmetadata.service.migration.postgres.v180;
|
||||
|
||||
import static org.openmetadata.service.migration.utils.v180.MigrationUtil.addCertificationOperationsToPolicy;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.openmetadata.service.migration.api.MigrationProcessImpl;
|
||||
import org.openmetadata.service.migration.utils.MigrationFile;
|
||||
|
||||
public class Migration extends MigrationProcessImpl {
|
||||
|
||||
public Migration(MigrationFile migrationFile) {
|
||||
super(migrationFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void runDataMigration() {
|
||||
addCertificationOperationsToPolicy(collectionDAO);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package org.openmetadata.service.migration.utils.v180;
|
||||
|
||||
import static org.openmetadata.service.migration.utils.v160.MigrationUtil.addOperationsToPolicyRule;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openmetadata.schema.type.MetadataOperation;
|
||||
import org.openmetadata.service.jdbi3.CollectionDAO;
|
||||
|
||||
@Slf4j
|
||||
public class MigrationUtil {
|
||||
|
||||
public static void addCertificationOperationsToPolicy(CollectionDAO collectionDAO) {
|
||||
|
||||
addOperationsToPolicyRule(
|
||||
"DataConsumerPolicy",
|
||||
"DataConsumerPolicy-EditRule",
|
||||
List.of(MetadataOperation.EDIT_CERTIFICATION),
|
||||
collectionDAO);
|
||||
|
||||
addOperationsToPolicyRule(
|
||||
"DataStewardPolicy",
|
||||
"DataStewardPolicy-EditRule",
|
||||
List.of(MetadataOperation.EDIT_CERTIFICATION),
|
||||
collectionDAO);
|
||||
}
|
||||
}
|
||||
@ -79,7 +79,7 @@ public class DatabaseResource extends EntityResource<Database, DatabaseRepositor
|
||||
public static final String COLLECTION_PATH = "v1/databases/";
|
||||
private final DatabaseMapper mapper = new DatabaseMapper();
|
||||
static final String FIELDS =
|
||||
"owners,databaseSchemas,usageSummary,location,tags,extension,domain,sourceHash,followers";
|
||||
"owners,databaseSchemas,usageSummary,location,tags,certification,extension,domain,sourceHash,followers";
|
||||
|
||||
@Override
|
||||
public Database addHref(UriInfo uriInfo, Database db) {
|
||||
|
||||
@ -78,7 +78,7 @@ public class DatabaseSchemaResource
|
||||
private final DatabaseSchemaMapper mapper = new DatabaseSchemaMapper();
|
||||
public static final String COLLECTION_PATH = "v1/databaseSchemas/";
|
||||
static final String FIELDS =
|
||||
"owners,tables,usageSummary,tags,extension,domain,sourceHash,followers";
|
||||
"owners,tables,usageSummary,tags,certification,extension,domain,sourceHash,followers";
|
||||
|
||||
@Override
|
||||
public DatabaseSchema addHref(UriInfo uriInfo, DatabaseSchema schema) {
|
||||
|
||||
@ -15,6 +15,7 @@ import static org.openmetadata.service.Entity.RAW_COST_ANALYSIS_REPORT_DATA;
|
||||
import static org.openmetadata.service.Entity.WEB_ANALYTIC_ENTITY_VIEW_REPORT_DATA;
|
||||
import static org.openmetadata.service.Entity.WEB_ANALYTIC_USER_ACTIVITY_REPORT_DATA;
|
||||
import static org.openmetadata.service.search.SearchClient.ADD_OWNERS_SCRIPT;
|
||||
import static org.openmetadata.service.search.SearchClient.DATA_ASSET_SEARCH_ALIAS;
|
||||
import static org.openmetadata.service.search.SearchClient.DEFAULT_UPDATE_SCRIPT;
|
||||
import static org.openmetadata.service.search.SearchClient.GLOBAL_SEARCH_ALIAS;
|
||||
import static org.openmetadata.service.search.SearchClient.PROPAGATE_ENTITY_REFERENCE_FIELD_SCRIPT;
|
||||
@ -70,6 +71,7 @@ import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -91,6 +93,7 @@ import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearch
|
||||
import org.openmetadata.schema.service.configuration.elasticsearch.NaturalLanguageSearchConfiguration;
|
||||
import org.openmetadata.schema.tests.DataQualityReport;
|
||||
import org.openmetadata.schema.tests.TestSuite;
|
||||
import org.openmetadata.schema.type.AssetCertification;
|
||||
import org.openmetadata.schema.type.ChangeDescription;
|
||||
import org.openmetadata.schema.type.EntityReference;
|
||||
import org.openmetadata.schema.type.FieldChange;
|
||||
@ -106,6 +109,7 @@ import org.openmetadata.service.search.nlq.NLQService;
|
||||
import org.openmetadata.service.search.nlq.NLQServiceFactory;
|
||||
import org.openmetadata.service.search.opensearch.OpenSearchClient;
|
||||
import org.openmetadata.service.security.policyevaluator.SubjectContext;
|
||||
import org.openmetadata.service.util.EntityUtil;
|
||||
import org.openmetadata.service.util.FullyQualifiedName;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
import org.openmetadata.service.workflows.searchIndex.ReindexingUtil;
|
||||
@ -124,7 +128,7 @@ public class SearchRepository {
|
||||
private final List<String> inheritableFields =
|
||||
List.of(
|
||||
FIELD_OWNERS,
|
||||
Entity.FIELD_DOMAIN,
|
||||
FIELD_DOMAIN,
|
||||
Entity.FIELD_DISABLED,
|
||||
Entity.FIELD_TEST_SUITES,
|
||||
FIELD_DISPLAY_NAME);
|
||||
@ -528,22 +532,81 @@ public class SearchRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private static final String CERTIFICATION = "Certification";
|
||||
private static final String CERTIFICATION_FIELD = "certification";
|
||||
private static final String CERTIFICATION_TAG_FQN_FIELD = "certification.tagLabel.tagFQN";
|
||||
|
||||
public void propagateCertificationTags(
|
||||
String entityType, EntityInterface entity, ChangeDescription changeDescription) {
|
||||
if (changeDescription != null && entityType.equalsIgnoreCase(Entity.TAG)) {
|
||||
Tag tagEntity = (Tag) entity;
|
||||
if (tagEntity.getClassification().getFullyQualifiedName().equals("Certification")) {
|
||||
Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("name", entity.getName());
|
||||
paramMap.put("description", entity.getDescription());
|
||||
paramMap.put("tagFQN", entity.getFullyQualifiedName());
|
||||
paramMap.put("style", entity.getStyle());
|
||||
searchClient.updateChildren(
|
||||
GLOBAL_SEARCH_ALIAS,
|
||||
new ImmutablePair<>("certification.tagLabel.tagFQN", entity.getFullyQualifiedName()),
|
||||
new ImmutablePair<>(UPDATE_CERTIFICATION_SCRIPT, paramMap));
|
||||
}
|
||||
if (changeDescription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Entity.TAG.equalsIgnoreCase(entityType)) {
|
||||
handleTagEntityUpdate((Tag) entity);
|
||||
} else {
|
||||
handleEntityCertificationUpdate(entity, changeDescription);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTagEntityUpdate(Tag tagEntity) {
|
||||
if (CERTIFICATION.equals(tagEntity.getClassification().getFullyQualifiedName())) {
|
||||
updateCertificationInSearch(tagEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCertificationInSearch(Tag tagEntity) {
|
||||
Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("name", tagEntity.getName());
|
||||
paramMap.put("description", tagEntity.getDescription());
|
||||
paramMap.put("tagFQN", tagEntity.getFullyQualifiedName());
|
||||
paramMap.put("style", tagEntity.getStyle());
|
||||
searchClient.updateChildren(
|
||||
DATA_ASSET_SEARCH_ALIAS,
|
||||
new ImmutablePair<>(CERTIFICATION_TAG_FQN_FIELD, tagEntity.getFullyQualifiedName()),
|
||||
new ImmutablePair<>(UPDATE_CERTIFICATION_SCRIPT, paramMap));
|
||||
}
|
||||
|
||||
private void handleEntityCertificationUpdate(EntityInterface entity, ChangeDescription change) {
|
||||
if (!isCertificationUpdated(change)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AssetCertification certification = getCertificationFromEntity(entity);
|
||||
updateEntityCertificationInSearch(entity, certification);
|
||||
}
|
||||
|
||||
private boolean isCertificationUpdated(ChangeDescription change) {
|
||||
return Stream.concat(
|
||||
Stream.concat(change.getFieldsUpdated().stream(), change.getFieldsAdded().stream()),
|
||||
change.getFieldsDeleted().stream())
|
||||
.anyMatch(fieldChange -> CERTIFICATION_FIELD.equals(fieldChange.getName()));
|
||||
}
|
||||
|
||||
private AssetCertification getCertificationFromEntity(EntityInterface entity) {
|
||||
return (AssetCertification) EntityUtil.getEntityField(entity, CERTIFICATION_FIELD);
|
||||
}
|
||||
|
||||
private void updateEntityCertificationInSearch(
|
||||
EntityInterface entity, AssetCertification certification) {
|
||||
IndexMapping indexMapping = entityIndexMap.get(entity.getEntityReference().getType());
|
||||
String indexName = indexMapping.getIndexName(clusterAlias);
|
||||
Map<String, Object> paramMap = new HashMap<>();
|
||||
|
||||
if (certification != null && certification.getTagLabel() != null) {
|
||||
paramMap.put("name", certification.getTagLabel().getName());
|
||||
paramMap.put("description", certification.getTagLabel().getDescription());
|
||||
paramMap.put("tagFQN", certification.getTagLabel().getTagFQN());
|
||||
paramMap.put("style", certification.getTagLabel().getStyle());
|
||||
} else {
|
||||
paramMap.put("name", null);
|
||||
paramMap.put("description", null);
|
||||
paramMap.put("tagFQN", null);
|
||||
paramMap.put("style", null);
|
||||
}
|
||||
|
||||
searchClient.updateEntity(
|
||||
indexName, entity.getId().toString(), paramMap, UPDATE_CERTIFICATION_SCRIPT);
|
||||
}
|
||||
|
||||
public void propagateToRelatedEntities(
|
||||
@ -622,7 +685,7 @@ public class SearchRepository {
|
||||
searchClient.updateByFqnPrefix(GLOBAL_SEARCH_ALIAS, oldFQN, newFQN, TAGS_FQN);
|
||||
}
|
||||
|
||||
if (field.getName().equalsIgnoreCase(Entity.FIELD_DISPLAY_NAME)) {
|
||||
if (field.getName().equalsIgnoreCase(FIELD_DISPLAY_NAME)) {
|
||||
Map<String, Object> updates = new HashMap<>();
|
||||
updates.put("displayName", field.getNewValue().toString());
|
||||
paramMap.put("tagFQN", oldFQN);
|
||||
@ -692,7 +755,7 @@ public class SearchRepository {
|
||||
if (field.getName().equals(Entity.FIELD_TEST_SUITES)) {
|
||||
scriptTxt.append(PROPAGATE_TEST_SUITES_SCRIPT);
|
||||
fieldData.put(Entity.FIELD_TEST_SUITES, field.getNewValue());
|
||||
} else if (field.getName().equals(Entity.FIELD_DISPLAY_NAME)) {
|
||||
} else if (field.getName().equals(FIELD_DISPLAY_NAME)) {
|
||||
String fieldPath =
|
||||
getFieldPath(entity.getEntityReference().getType(), field.getName());
|
||||
fieldData.put(field.getName(), field.getNewValue().toString());
|
||||
@ -732,7 +795,7 @@ public class SearchRepository {
|
||||
}
|
||||
scriptTxt.append(" ");
|
||||
} catch (UnhandledServerException e) {
|
||||
if (field.getName().equals(Entity.FIELD_DISPLAY_NAME)) {
|
||||
if (field.getName().equals(FIELD_DISPLAY_NAME)) {
|
||||
String fieldPath =
|
||||
getFieldPath(entity.getEntityReference().getType(), field.getName());
|
||||
fieldData.put(field.getName(), field.getNewValue().toString());
|
||||
|
||||
@ -137,6 +137,8 @@ public class JsonPatchUtils {
|
||||
String tagFQN = tag.getTagFQN();
|
||||
if (isTierClassification(tagFQN)) {
|
||||
return MetadataOperation.EDIT_TIER;
|
||||
} else if (isCertificationClassification(tagFQN)) {
|
||||
return MetadataOperation.EDIT_CERTIFICATION;
|
||||
} else if ("Classification".equalsIgnoreCase(source)) {
|
||||
return MetadataOperation.EDIT_TAGS;
|
||||
} else if ("Glossary".equalsIgnoreCase(source)) {
|
||||
@ -150,6 +152,10 @@ public class JsonPatchUtils {
|
||||
return tagFQN != null && tagFQN.startsWith("Tier.");
|
||||
}
|
||||
|
||||
private static boolean isCertificationClassification(String tagFQN) {
|
||||
return tagFQN != null && tagFQN.startsWith("Certification.");
|
||||
}
|
||||
|
||||
public static MetadataOperation getMetadataOperation(Object jsonPatchObject) {
|
||||
String path;
|
||||
|
||||
|
||||
@ -61,6 +61,16 @@
|
||||
"`Tier.Tier2`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "certification",
|
||||
"required": false,
|
||||
"description": "Certification tag from the Certification classification. Only one certification level can be applied.",
|
||||
"examples": [
|
||||
"Certification.Gold",
|
||||
"Certification.Silver",
|
||||
"Certification.Bronze"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retentionPeriod",
|
||||
"required": false,
|
||||
|
||||
@ -61,6 +61,16 @@
|
||||
"`Tier.Tier2`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "certification",
|
||||
"required": false,
|
||||
"description": "Certification tag from the Certification classification. Only one certification level can be applied.",
|
||||
"examples": [
|
||||
"Certification.Gold",
|
||||
"Certification.Silver",
|
||||
"Certification.Bronze"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retentionPeriod",
|
||||
"required": false,
|
||||
|
||||
@ -61,6 +61,16 @@
|
||||
"`Tier.Tier2`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "certification",
|
||||
"required": false,
|
||||
"description": "Certification tag from the Certification classification. Only one certification level can be applied.",
|
||||
"examples": [
|
||||
"Certification.Gold",
|
||||
"Certification.Silver",
|
||||
"Certification.Bronze"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"required": false,
|
||||
|
||||
@ -62,6 +62,16 @@
|
||||
"`Tier.Tier2`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "certification",
|
||||
"required": false,
|
||||
"description": "Certification tag from the Certification classification. Only one certification level can be applied.",
|
||||
"examples": [
|
||||
"Certification.Gold",
|
||||
"Certification.Silver",
|
||||
"Certification.Bronze"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "retentionPeriod",
|
||||
"required": false,
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
"name": "DataConsumerPolicy-EditRule",
|
||||
"description" : "Allow some of the edit operations on a resource for everyone.",
|
||||
"resources" : ["all"],
|
||||
"operations": ["ViewAll", "EditDescription", "EditTags", "EditGlossaryTerms", "EditTier"],
|
||||
"operations": ["ViewAll", "EditDescription", "EditTags", "EditGlossaryTerms", "EditTier", "EditCertification"],
|
||||
"effect": "allow"
|
||||
}
|
||||
]
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
{
|
||||
"name": "DataStewardPolicy-EditRule",
|
||||
"resources" : ["all"],
|
||||
"operations": ["ViewAll", "EditDescription", "EditDisplayName","EditLineage","EditOwners", "EditTags", "EditTier", "EditGlossaryTerms"],
|
||||
"operations": ["ViewAll", "EditDescription", "EditDisplayName", "EditLineage", "EditOwners", "EditTags", "EditTier", "EditGlossaryTerms", "EditCertification"],
|
||||
"effect": "allow"
|
||||
}
|
||||
]
|
||||
|
||||
@ -51,6 +51,7 @@ import org.openmetadata.csv.EntityCsv;
|
||||
import org.openmetadata.schema.api.data.CreateDatabase;
|
||||
import org.openmetadata.schema.api.data.CreateDatabaseSchema;
|
||||
import org.openmetadata.schema.api.data.CreateTable;
|
||||
import org.openmetadata.schema.entity.classification.Tag;
|
||||
import org.openmetadata.schema.entity.data.Database;
|
||||
import org.openmetadata.schema.entity.data.DatabaseSchema;
|
||||
import org.openmetadata.schema.entity.data.Table;
|
||||
@ -60,6 +61,7 @@ import org.openmetadata.schema.type.csv.CsvImportResult;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.resources.EntityResourceTest;
|
||||
import org.openmetadata.service.resources.databases.DatabaseResource.DatabaseList;
|
||||
import org.openmetadata.service.resources.tags.TagResourceTest;
|
||||
import org.openmetadata.service.util.FullyQualifiedName;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
import org.openmetadata.service.util.ResultList;
|
||||
@ -129,7 +131,7 @@ public class DatabaseResourceTest extends EntityResourceTest<Database, CreateDat
|
||||
// Update databaseSchema with invalid tags field
|
||||
String resultsHeader =
|
||||
recordToString(EntityCsv.getResultHeaders(getDatabaseCsvHeaders(database, false)));
|
||||
String record = "s1,dsp1,dsc1,,Tag.invalidTag,,,,,,";
|
||||
String record = "s1,dsp1,dsc1,,Tag.invalidTag,,,,,,,";
|
||||
String csv = createCsv(getDatabaseCsvHeaders(database, false), listOf(record), null);
|
||||
CsvImportResult result = importCsv(databaseName, csv, false);
|
||||
assertSummary(result, ApiStatus.PARTIAL_SUCCESS, 2, 1, 1);
|
||||
@ -140,7 +142,7 @@ public class DatabaseResourceTest extends EntityResourceTest<Database, CreateDat
|
||||
assertRows(result, expectedRows);
|
||||
|
||||
// invalid tag it will give error.
|
||||
record = "non-existing,dsp1,dsc1,,Tag.invalidTag,,,,,,";
|
||||
record = "non-existing,dsp1,dsc1,,Tag.invalidTag,,,,,,,";
|
||||
csv = createCsv(getDatabaseSchemaCsvHeaders(dbSchema, false), listOf(record), null);
|
||||
result = importCsv(databaseName, csv, false);
|
||||
assertSummary(result, ApiStatus.PARTIAL_SUCCESS, 2, 1, 1);
|
||||
@ -152,7 +154,7 @@ public class DatabaseResourceTest extends EntityResourceTest<Database, CreateDat
|
||||
|
||||
// databaseSchema will be created if it does not exist
|
||||
String schemaFqn = FullyQualifiedName.add(database.getFullyQualifiedName(), "non-existing");
|
||||
record = "non-existing,dsp1,dsc1,,,,,,,,";
|
||||
record = "non-existing,dsp1,dsc1,,,,,,,,,";
|
||||
csv = createCsv(getDatabaseSchemaCsvHeaders(dbSchema, false), listOf(record), null);
|
||||
result = importCsv(databaseName, csv, false);
|
||||
assertSummary(result, ApiStatus.SUCCESS, 2, 2, 0);
|
||||
@ -171,13 +173,22 @@ public class DatabaseResourceTest extends EntityResourceTest<Database, CreateDat
|
||||
schemaTest.createRequest("s1").withDatabase(database.getFullyQualifiedName());
|
||||
schemaTest.createEntity(createSchema, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Headers: name, displayName, description, owner, tags, glossaryTerms, tiers, retentionPeriod,
|
||||
// Create certification
|
||||
TagResourceTest tagResourceTest = new TagResourceTest();
|
||||
Tag certificationTag =
|
||||
tagResourceTest.createEntity(
|
||||
tagResourceTest.createRequest("Certification"), ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Headers: name, displayName, description, owner, tags, glossaryTerms, tiers, certification,
|
||||
// retentionPeriod,
|
||||
// sourceUrl, domain
|
||||
// Update terms with change in description
|
||||
String record =
|
||||
String.format(
|
||||
"s1,dsp1,new-dsc1,user:%s,,,Tier.Tier1,P23DT23H,http://test.com,%s,",
|
||||
user1, escapeCsv(DOMAIN.getFullyQualifiedName()));
|
||||
"s1,dsp1,new-dsc1,user:%s,,,Tier.Tier1,%s,P23DT23H,http://test.com,%s,",
|
||||
user1,
|
||||
certificationTag.getFullyQualifiedName(),
|
||||
escapeCsv(DOMAIN.getFullyQualifiedName()));
|
||||
|
||||
// Update created entity with changes
|
||||
importCsvAndValidate(
|
||||
@ -186,7 +197,7 @@ public class DatabaseResourceTest extends EntityResourceTest<Database, CreateDat
|
||||
null,
|
||||
listOf(record));
|
||||
|
||||
String clearRecord = "s1,dsp1,new-dsc2,,,,,P23DT23H,http://test.com,,";
|
||||
String clearRecord = "s1,dsp1,new-dsc2,,,,,,P23DT23H,http://test.com,,";
|
||||
importCsvAndValidate(
|
||||
database.getFullyQualifiedName(),
|
||||
getDatabaseCsvHeaders(database, false),
|
||||
|
||||
@ -49,6 +49,7 @@ import org.openmetadata.csv.EntityCsv;
|
||||
import org.openmetadata.schema.api.data.CreateDatabaseSchema;
|
||||
import org.openmetadata.schema.api.data.CreateTable;
|
||||
import org.openmetadata.schema.api.data.RestoreEntity;
|
||||
import org.openmetadata.schema.entity.classification.Tag;
|
||||
import org.openmetadata.schema.entity.data.DatabaseSchema;
|
||||
import org.openmetadata.schema.entity.data.Table;
|
||||
import org.openmetadata.schema.type.ApiStatus;
|
||||
@ -57,6 +58,7 @@ import org.openmetadata.schema.type.csv.CsvImportResult;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.resources.EntityResourceTest;
|
||||
import org.openmetadata.service.resources.databases.DatabaseSchemaResource.DatabaseSchemaList;
|
||||
import org.openmetadata.service.resources.tags.TagResourceTest;
|
||||
import org.openmetadata.service.util.FullyQualifiedName;
|
||||
import org.openmetadata.service.util.TestUtils;
|
||||
|
||||
@ -121,11 +123,12 @@ public class DatabaseSchemaResourceTest
|
||||
tableTest.createRequest("s1").withDatabaseSchema(schema.getFullyQualifiedName());
|
||||
tableTest.createEntity(createTable, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Headers: name, displayName, description, owner, tags, retentionPeriod, sourceUrl, domain
|
||||
// Headers: name, displayName, description, owner, tags, glossaryTerms, tiers, certification,
|
||||
// retentionPeriod, sourceUrl, domain, extension
|
||||
// Create table with invalid tags field
|
||||
String resultsHeader =
|
||||
recordToString(EntityCsv.getResultHeaders(getDatabaseSchemaCsvHeaders(schema, false)));
|
||||
String record = "s1,dsp1,dsc1,,Tag.invalidTag,,,,,,";
|
||||
String record = "s1,dsp1,dsc1,,Tag.invalidTag,,,,,,,";
|
||||
String csv = createCsv(getDatabaseSchemaCsvHeaders(schema, false), listOf(record), null);
|
||||
CsvImportResult result = importCsv(schemaName, csv, false);
|
||||
assertSummary(result, ApiStatus.PARTIAL_SUCCESS, 2, 1, 1);
|
||||
@ -136,7 +139,7 @@ public class DatabaseSchemaResourceTest
|
||||
assertRows(result, expectedRows);
|
||||
|
||||
// Tag will cause failure
|
||||
record = "non-existing,dsp1,dsc1,,Tag.invalidTag,,,,,,";
|
||||
record = "non-existing,dsp1,dsc1,,Tag.invalidTag,,,,,,,";
|
||||
csv = createCsv(getDatabaseSchemaCsvHeaders(schema, false), listOf(record), null);
|
||||
result = importCsv(schemaName, csv, false);
|
||||
assertSummary(result, ApiStatus.PARTIAL_SUCCESS, 2, 1, 1);
|
||||
@ -147,7 +150,7 @@ public class DatabaseSchemaResourceTest
|
||||
assertRows(result, expectedRows);
|
||||
|
||||
// non-existing table will cause
|
||||
record = "non-existing,dsp1,dsc1,,,,,,,,";
|
||||
record = "non-existing,dsp1,dsc1,,,,,,,,,";
|
||||
String tableFqn = FullyQualifiedName.add(schema.getFullyQualifiedName(), "non-existing");
|
||||
csv = createCsv(getDatabaseSchemaCsvHeaders(schema, false), listOf(record), null);
|
||||
result = importCsv(schemaName, csv, false);
|
||||
@ -167,13 +170,20 @@ public class DatabaseSchemaResourceTest
|
||||
tableTest.createRequest("s1").withDatabaseSchema(schema.getFullyQualifiedName());
|
||||
tableTest.createEntity(createTable, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Create certification
|
||||
TagResourceTest tagResourceTest = new TagResourceTest();
|
||||
Tag certificationTag =
|
||||
tagResourceTest.createEntity(
|
||||
tagResourceTest.createRequest("Certification"), ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Headers: name, displayName, description, owner, tags, retentionPeriod, sourceUrl, domain
|
||||
// Update terms with change in description
|
||||
List<String> updateRecords =
|
||||
listOf(
|
||||
String.format(
|
||||
"s1,dsp1,new-dsc1,user:%s,,,Tier.Tier1,P23DT23H,http://test.com,%s,",
|
||||
user1, escapeCsv(DOMAIN.getFullyQualifiedName())));
|
||||
"s1,dsp1,new-dsc1,user:%s,,,Tier.Tier1,%s,P23DT23H,http://test.com,%s,",
|
||||
user1,
|
||||
certificationTag.getFullyQualifiedName(),
|
||||
escapeCsv(DOMAIN.getFullyQualifiedName())));
|
||||
|
||||
// Update created entity with changes
|
||||
importCsvAndValidate(
|
||||
@ -182,7 +192,7 @@ public class DatabaseSchemaResourceTest
|
||||
null,
|
||||
updateRecords);
|
||||
|
||||
List<String> clearRecords = listOf("s1,dsp1,new-dsc2,,,,,P23DT23H,http://test.com,,");
|
||||
List<String> clearRecords = listOf("s1,dsp1,new-dsc2,,,,,,P23DT23H,http://test.com,,");
|
||||
|
||||
importCsvAndValidate(
|
||||
schema.getFullyQualifiedName(),
|
||||
@ -244,7 +254,8 @@ public class DatabaseSchemaResourceTest
|
||||
|
||||
// Validate updated table
|
||||
Table updated =
|
||||
tableTest.getEntityByName(table.getFullyQualifiedName(), "description", ADMIN_AUTH_HEADERS);
|
||||
tableTest.getEntityByName(
|
||||
table.getFullyQualifiedName(), "description,certification", ADMIN_AUTH_HEADERS);
|
||||
assertEquals("Updated Table Description", updated.getDescription());
|
||||
|
||||
// Validate updated column
|
||||
|
||||
@ -24,8 +24,11 @@ import static org.openmetadata.common.utils.CommonUtil.listOf;
|
||||
import static org.openmetadata.common.utils.CommonUtil.listOrEmpty;
|
||||
import static org.openmetadata.csv.CsvUtil.recordToString;
|
||||
import static org.openmetadata.csv.EntityCsv.entityNotFound;
|
||||
import static org.openmetadata.csv.EntityCsvTest.*;
|
||||
import static org.openmetadata.csv.EntityCsvTest.assertRows;
|
||||
import static org.openmetadata.csv.EntityCsvTest.assertSummary;
|
||||
import static org.openmetadata.csv.EntityCsvTest.createCsv;
|
||||
import static org.openmetadata.csv.EntityCsvTest.getFailedRecord;
|
||||
import static org.openmetadata.csv.EntityCsvTest.getSuccessRecord;
|
||||
import static org.openmetadata.service.exception.CatalogExceptionMessage.invalidEnumValue;
|
||||
import static org.openmetadata.service.util.EntityUtil.fieldAdded;
|
||||
import static org.openmetadata.service.util.EntityUtil.fieldUpdated;
|
||||
@ -86,6 +89,7 @@ import org.openmetadata.service.resources.databases.TableResourceTest;
|
||||
import org.openmetadata.service.resources.services.database.DatabaseServiceResource;
|
||||
import org.openmetadata.service.resources.services.database.DatabaseServiceResource.DatabaseServiceList;
|
||||
import org.openmetadata.service.resources.services.ingestionpipelines.IngestionPipelineResourceTest;
|
||||
import org.openmetadata.service.resources.tags.TagResourceTest;
|
||||
import org.openmetadata.service.secrets.masker.PasswordEntityMasker;
|
||||
import org.openmetadata.service.util.FullyQualifiedName;
|
||||
import org.openmetadata.service.util.JsonUtils;
|
||||
@ -359,7 +363,7 @@ public class DatabaseServiceResourceTest
|
||||
// Update database with invalid tags field
|
||||
String resultsHeader =
|
||||
recordToString(EntityCsv.getResultHeaders(getDatabaseServiceCsvHeaders(service, false)));
|
||||
String record = "d1,dsp1,dsc1,,Tag.invalidTag,,,,";
|
||||
String record = "d1,dsp1,dsc1,,Tag.invalidTag,,,,,";
|
||||
String csv = createCsv(getDatabaseServiceCsvHeaders(service, false), listOf(record), null);
|
||||
CsvImportResult result = importCsv(serviceName, csv, false);
|
||||
assertSummary(result, ApiStatus.PARTIAL_SUCCESS, 2, 1, 1);
|
||||
@ -370,7 +374,7 @@ public class DatabaseServiceResourceTest
|
||||
assertRows(result, expectedRows);
|
||||
|
||||
// invalid tag it will give error.
|
||||
record = "non-existing,dsp1,dsc1,,Tag.invalidTag,,,,";
|
||||
record = "non-existing,dsp1,dsc1,,Tag.invalidTag,,,,,";
|
||||
csv = createCsv(getDatabaseServiceCsvHeaders(service, false), listOf(record), null);
|
||||
result = importCsv(serviceName, csv, false);
|
||||
assertSummary(result, ApiStatus.PARTIAL_SUCCESS, 2, 1, 1);
|
||||
@ -382,7 +386,7 @@ public class DatabaseServiceResourceTest
|
||||
|
||||
// database will be created if it does not exist
|
||||
String databaseFqn = FullyQualifiedName.add(serviceName, "non-existing");
|
||||
record = "non-existing,dsp1,dsc1,,,,,,";
|
||||
record = "non-existing,dsp1,dsc1,,,,,,,";
|
||||
csv = createCsv(getDatabaseServiceCsvHeaders(service, false), listOf(record), null);
|
||||
result = importCsv(serviceName, csv, false);
|
||||
assertSummary(result, ApiStatus.SUCCESS, 2, 2, 0);
|
||||
@ -401,11 +405,15 @@ public class DatabaseServiceResourceTest
|
||||
databaseTest.createRequest("d1").withService(service.getFullyQualifiedName());
|
||||
databaseTest.createEntity(createDatabase, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Headers: name, displayName, description, owner, tags, glossaryTerms, tiers, domain, extension
|
||||
// Create certification
|
||||
TagResourceTest tagResourceTest = new TagResourceTest();
|
||||
|
||||
// Headers: name, displayName, description, owner, tags, glossaryTerms, tiers,
|
||||
// certification,domain, extension
|
||||
// Update terms with change in description
|
||||
String record =
|
||||
String.format(
|
||||
"d1,dsp1,new-dsc1,user:%s,,,Tier.Tier1,%s,",
|
||||
"d1,dsp1,new-dsc1,user:%s,,,Tier.Tier1,,%s,",
|
||||
user1, escapeCsv(DOMAIN.getFullyQualifiedName()));
|
||||
|
||||
// Update created entity with changes
|
||||
@ -415,7 +423,7 @@ public class DatabaseServiceResourceTest
|
||||
null,
|
||||
listOf(record));
|
||||
|
||||
String clearRecord = "d1,dsp1,new-dsc2,,,,,,";
|
||||
String clearRecord = "d1,dsp1,new-dsc2,,,,,,,";
|
||||
|
||||
importCsvAndValidate(
|
||||
service.getFullyQualifiedName(),
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package org.openmetadata.service.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.openmetadata.common.utils.CommonUtil.listOf;
|
||||
|
||||
@ -13,6 +14,7 @@ import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@ -276,4 +278,93 @@ class JsonPatchUtilsTest {
|
||||
"MetadataOperations should contain EDIT_TAGS");
|
||||
assertEquals(1, operations.size(), "There should be exactly one MetadataOperation");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddCertificationTag() throws JsonPatchException, IOException {
|
||||
// Create a patch to add a new Certification tag
|
||||
long currentTime = System.currentTimeMillis();
|
||||
String patchString = getPatchString(currentTime, " \"op\": \"add\",\n");
|
||||
JsonPatch patch;
|
||||
try (JsonReader jsonReader = Json.createReader(new StringReader(patchString))) {
|
||||
JsonArray patchArray = jsonReader.readArray();
|
||||
patch = Json.createPatch(patchArray);
|
||||
}
|
||||
|
||||
// Determine MetadataOperations
|
||||
Set<MetadataOperation> operations =
|
||||
JsonPatchUtils.getMetadataOperations(resourceContextMock, patch);
|
||||
|
||||
// Assertions
|
||||
assertTrue(operations.contains(MetadataOperation.EDIT_CERTIFICATION));
|
||||
assertEquals(1, operations.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReplaceCertificationTag() throws JsonPatchException, IOException {
|
||||
// Create a patch to replace the Certification tag
|
||||
long currentTime = System.currentTimeMillis();
|
||||
String patchString = getPatchString(currentTime, " \"op\": \"replace\",\n");
|
||||
JsonPatch patch;
|
||||
try (JsonReader jsonReader = Json.createReader(new StringReader(patchString))) {
|
||||
JsonArray patchArray = jsonReader.readArray();
|
||||
patch = Json.createPatch(patchArray);
|
||||
}
|
||||
|
||||
// Determine MetadataOperations
|
||||
Set<MetadataOperation> operations =
|
||||
JsonPatchUtils.getMetadataOperations(resourceContextMock, patch);
|
||||
|
||||
// Assertions
|
||||
assertTrue(operations.contains(MetadataOperation.EDIT_CERTIFICATION));
|
||||
assertEquals(1, operations.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveCertificationTag() throws JsonPatchException, IOException {
|
||||
// Create a patch to remove the Certification tag
|
||||
String patchString =
|
||||
"""
|
||||
[
|
||||
{
|
||||
"op": "remove",
|
||||
"path": "/certification"
|
||||
}
|
||||
]""";
|
||||
JsonPatch patch;
|
||||
try (JsonReader jsonReader = Json.createReader(new StringReader(patchString))) {
|
||||
JsonArray patchArray = jsonReader.readArray();
|
||||
patch = Json.createPatch(patchArray);
|
||||
}
|
||||
|
||||
// Determine MetadataOperations
|
||||
Set<MetadataOperation> operations =
|
||||
JsonPatchUtils.getMetadataOperations(resourceContextMock, patch);
|
||||
// Assertions
|
||||
assertTrue(operations.contains(MetadataOperation.EDIT_CERTIFICATION));
|
||||
assertEquals(1, operations.size());
|
||||
}
|
||||
|
||||
private static @NotNull String getPatchString(long currentTime, String operationString) {
|
||||
long oneYearInMillis = 365L * 24 * 60 * 60 * 1000;
|
||||
long expiryTime = currentTime + oneYearInMillis;
|
||||
return "[\n"
|
||||
+ " {\n"
|
||||
+ operationString
|
||||
+ " \"path\": \"/certification\",\n"
|
||||
+ " \"value\": {\n"
|
||||
+ " \"tagLabel\": {\n"
|
||||
+ " \"tagFQN\": \"Certification.Gold\",\n"
|
||||
+ " \"labelType\": \"Manual\",\n"
|
||||
+ " \"state\": \"Confirmed\"\n"
|
||||
+ " },\n"
|
||||
+ " \"appliedDate\": "
|
||||
+ currentTime
|
||||
+ ",\n"
|
||||
+ " \"expiryDate\": "
|
||||
+ expiryTime
|
||||
+ "\n"
|
||||
+ " }\n"
|
||||
+ " }\n"
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
"EditGlossaryTerms",
|
||||
"EditTeams",
|
||||
"EditTier",
|
||||
"EditCertification",
|
||||
"EditTests",
|
||||
"EditUsage",
|
||||
"EditUsers",
|
||||
|
||||
@ -32,6 +32,11 @@ export const FollowSupportedServices = [
|
||||
EntityTypeEndpoint.Database,
|
||||
];
|
||||
|
||||
export const CertificationSupportedServices = [
|
||||
EntityTypeEndpoint.DatabaseSchema,
|
||||
EntityTypeEndpoint.Database,
|
||||
];
|
||||
|
||||
export const VISIT_SERVICE_PAGE_DETAILS = {
|
||||
[SERVICE_TYPE.Database]: {
|
||||
settingsMenuId: GlobalSettingOptions.DATABASES,
|
||||
|
||||
@ -209,7 +209,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 12, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 13, 'ArrowLeft');
|
||||
|
||||
await fillRowDetails(
|
||||
{
|
||||
@ -238,7 +238,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 12, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 13, 'ArrowLeft');
|
||||
|
||||
// Fill table and columns details
|
||||
await fillRowDetails(
|
||||
@ -268,7 +268,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 12, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 13, 'ArrowLeft');
|
||||
|
||||
await fillRecursiveColumnDetails(
|
||||
{
|
||||
@ -287,7 +287,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 16, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 19, 'ArrowLeft');
|
||||
|
||||
await fillRowDetails(
|
||||
{
|
||||
@ -320,7 +320,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 18, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 19, 'ArrowLeft');
|
||||
|
||||
await fillRowDetails(
|
||||
{
|
||||
@ -478,7 +478,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 12, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 13, 'ArrowLeft');
|
||||
|
||||
// Fill table and columns details
|
||||
await fillRowDetails(
|
||||
@ -508,7 +508,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 12, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 13, 'ArrowLeft');
|
||||
|
||||
await fillRecursiveColumnDetails(
|
||||
{
|
||||
@ -527,7 +527,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 16, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 17, 'ArrowLeft');
|
||||
|
||||
await fillRowDetails(
|
||||
{
|
||||
@ -694,7 +694,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 12, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 13, 'ArrowLeft');
|
||||
|
||||
// Fill table columns details
|
||||
await fillRecursiveColumnDetails(
|
||||
@ -714,7 +714,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 16, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 17, 'ArrowLeft');
|
||||
|
||||
await fillRowDetails(
|
||||
{
|
||||
@ -744,7 +744,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 12, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 13, 'ArrowLeft');
|
||||
|
||||
// fill second table columns details
|
||||
await fillRecursiveColumnDetails(
|
||||
@ -859,7 +859,7 @@ test.describe('Bulk Import Export', () => {
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowDown', { delay: 100 });
|
||||
|
||||
await pressKeyXTimes(page, 8, 'ArrowLeft');
|
||||
await pressKeyXTimes(page, 9, 'ArrowLeft');
|
||||
|
||||
await fillColumnDetails(columnDetails2, page);
|
||||
|
||||
|
||||
@ -216,6 +216,15 @@ entities.forEach((EntityClass) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Certification Add Remove', async ({ page }) => {
|
||||
await entity.certification(
|
||||
page,
|
||||
EntityDataClass.certificationTag1,
|
||||
EntityDataClass.certificationTag2,
|
||||
entity
|
||||
);
|
||||
});
|
||||
|
||||
if (['Dashboard', 'Dashboard Data Model'].includes(entityName)) {
|
||||
test(`${entityName} page should show the project name`, async ({
|
||||
page,
|
||||
|
||||
@ -12,7 +12,10 @@
|
||||
*/
|
||||
import { Page, test as base } from '@playwright/test';
|
||||
import { CustomPropertySupportedEntityList } from '../../constant/customProperty';
|
||||
import { FollowSupportedServices } from '../../constant/service';
|
||||
import {
|
||||
CertificationSupportedServices,
|
||||
FollowSupportedServices,
|
||||
} from '../../constant/service';
|
||||
import { ApiCollectionClass } from '../../support/entity/ApiCollectionClass';
|
||||
import { DatabaseClass } from '../../support/entity/DatabaseClass';
|
||||
import { DatabaseSchemaClass } from '../../support/entity/DatabaseSchemaClass';
|
||||
@ -115,6 +118,16 @@ entities.forEach((EntityClass) => {
|
||||
await entity.tier(page, 'Tier1', 'Tier5');
|
||||
});
|
||||
|
||||
if (CertificationSupportedServices.includes(entity.endpoint)) {
|
||||
test('Certification Add Remove', async ({ page }) => {
|
||||
await entity.certification(
|
||||
page,
|
||||
EntityDataClass.certificationTag1,
|
||||
EntityDataClass.certificationTag2
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('Update description', async ({ page }) => {
|
||||
await entity.descriptionUpdate(page);
|
||||
});
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
import {
|
||||
addMultiOwner,
|
||||
addOwner,
|
||||
assignCertification,
|
||||
assignGlossaryTerm,
|
||||
assignGlossaryTermToChildren,
|
||||
assignTag,
|
||||
@ -41,6 +42,7 @@ import {
|
||||
downVote,
|
||||
followEntity,
|
||||
hardDeleteEntity,
|
||||
removeCertification,
|
||||
removeDisplayNameForEntityChildren,
|
||||
removeGlossaryTerm,
|
||||
removeGlossaryTermFromChildren,
|
||||
@ -62,6 +64,7 @@ import {
|
||||
import { DataProduct } from '../domain/DataProduct';
|
||||
import { Domain } from '../domain/Domain';
|
||||
import { GlossaryTerm } from '../glossary/GlossaryTerm';
|
||||
import { TagClass } from '../tag/TagClass';
|
||||
import { EntityTypeEndpoint, ENTITY_PATH } from './Entity.interface';
|
||||
|
||||
export class EntityClass {
|
||||
@ -227,6 +230,35 @@ export class EntityClass {
|
||||
await removeTier(page, this.endpoint);
|
||||
}
|
||||
|
||||
async certification(
|
||||
page: Page,
|
||||
certification1: TagClass,
|
||||
certification2: TagClass,
|
||||
entity?: EntityClass
|
||||
) {
|
||||
await assignCertification(page, certification1, this.endpoint);
|
||||
if (entity) {
|
||||
await checkExploreSearchFilter(
|
||||
page,
|
||||
'Certification',
|
||||
'certification.tagLabel.tagFQN',
|
||||
certification1.responseData.fullyQualifiedName,
|
||||
entity
|
||||
);
|
||||
}
|
||||
await assignCertification(page, certification2, this.endpoint);
|
||||
if (entity) {
|
||||
await checkExploreSearchFilter(
|
||||
page,
|
||||
'Certification',
|
||||
'certification.tagLabel.tagFQN',
|
||||
certification2.responseData.fullyQualifiedName,
|
||||
entity
|
||||
);
|
||||
}
|
||||
await removeCertification(page, this.endpoint);
|
||||
}
|
||||
|
||||
async descriptionUpdate(page: Page) {
|
||||
const description =
|
||||
// eslint-disable-next-line max-len
|
||||
|
||||
@ -56,6 +56,12 @@ export class EntityDataClass {
|
||||
static readonly team2 = new TeamClass();
|
||||
static readonly tierTag1 = new TagClass({ classification: 'Tier' });
|
||||
static readonly tierTag2 = new TagClass({ classification: 'Tier' });
|
||||
static readonly certificationTag1 = new TagClass({
|
||||
classification: 'Certification',
|
||||
});
|
||||
static readonly certificationTag2 = new TagClass({
|
||||
classification: 'Certification',
|
||||
});
|
||||
static readonly classification1 = new ClassificationClass({
|
||||
provider: 'system',
|
||||
mutuallyExclusive: true,
|
||||
@ -120,6 +126,8 @@ export class EntityDataClass {
|
||||
this.team2.create(apiContext),
|
||||
this.tierTag1.create(apiContext),
|
||||
this.tierTag2.create(apiContext),
|
||||
this.certificationTag1.create(apiContext),
|
||||
this.certificationTag2.create(apiContext),
|
||||
this.classification1.create(apiContext),
|
||||
]
|
||||
: [];
|
||||
@ -234,6 +242,8 @@ export class EntityDataClass {
|
||||
this.team2.delete(apiContext),
|
||||
this.tierTag1.delete(apiContext),
|
||||
this.tierTag2.delete(apiContext),
|
||||
this.certificationTag1.delete(apiContext),
|
||||
this.certificationTag2.delete(apiContext),
|
||||
this.classification1.delete(apiContext),
|
||||
this.tag1.delete(apiContext),
|
||||
this.dataProduct1.delete(apiContext),
|
||||
|
||||
@ -22,6 +22,7 @@ import { ES_RESERVED_CHARACTERS } from '../constant/entity';
|
||||
import { SidebarItem } from '../constant/sidebar';
|
||||
import { EntityTypeEndpoint } from '../support/entity/Entity.interface';
|
||||
import { EntityClass } from '../support/entity/EntityClass';
|
||||
import { TagClass } from '../support/tag/TagClass';
|
||||
import {
|
||||
clickOutside,
|
||||
descriptionBox,
|
||||
@ -345,6 +346,39 @@ export const removeTier = async (page: Page, endpoint: string) => {
|
||||
await expect(page.getByTestId('Tier')).toContainText('No Tier');
|
||||
};
|
||||
|
||||
export const assignCertification = async (
|
||||
page: Page,
|
||||
certification: TagClass,
|
||||
endpoint: string
|
||||
) => {
|
||||
await page.getByTestId('edit-certification').click();
|
||||
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||
await page
|
||||
.getByTestId(`radio-btn-${certification.responseData.fullyQualifiedName}`)
|
||||
.click();
|
||||
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
||||
await page.getByTestId('update-certification').click();
|
||||
await patchRequest;
|
||||
await clickOutside(page);
|
||||
|
||||
await expect(page.getByTestId('certification-label')).toContainText(
|
||||
certification.responseData.displayName
|
||||
);
|
||||
};
|
||||
|
||||
export const removeCertification = async (page: Page, endpoint: string) => {
|
||||
await page.getByTestId('edit-certification').click();
|
||||
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||
const patchRequest = page.waitForResponse(`/api/v1/${endpoint}/*`);
|
||||
await page.getByTestId('clear-certification').click();
|
||||
await patchRequest;
|
||||
await clickOutside(page);
|
||||
|
||||
await expect(page.getByTestId('certification-label')).toContainText(
|
||||
'No Certification'
|
||||
);
|
||||
};
|
||||
|
||||
export const updateDescription = async (
|
||||
page: Page,
|
||||
description: string,
|
||||
@ -1567,6 +1601,22 @@ export const generateEntityChildren = (entityName: string, count = 25) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const checkItemNotExistsInQuickFilter = async (
|
||||
page: Page,
|
||||
filterLabel: string,
|
||||
filterValue: string
|
||||
) => {
|
||||
await sidebarClick(page, SidebarItem.EXPLORE);
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.click(`[data-testid="search-dropdown-${filterLabel}"]`);
|
||||
const testId = filterValue.toLowerCase();
|
||||
|
||||
// testId should not be present
|
||||
await expect(page.getByTestId(testId)).toBeHidden();
|
||||
|
||||
await clickOutside(page);
|
||||
};
|
||||
|
||||
export const checkExploreSearchFilter = async (
|
||||
page: Page,
|
||||
filterLabel: string,
|
||||
|
||||
@ -427,6 +427,7 @@ export const createDatabaseRowDetails = () => {
|
||||
entityType: 'Database',
|
||||
retentionPeriod: '1 year',
|
||||
sourceUrl: 'www.xyz.com',
|
||||
certification: 'Certification.Gold',
|
||||
};
|
||||
};
|
||||
|
||||
@ -442,6 +443,7 @@ export const createDatabaseSchemaRowDetails = () => {
|
||||
retentionPeriod: '1 year',
|
||||
sourceUrl: 'www.xy,z.com',
|
||||
entityType: 'Database Schema',
|
||||
certification: 'Certification.Gold',
|
||||
};
|
||||
};
|
||||
|
||||
@ -457,6 +459,7 @@ export const createTableRowDetails = () => {
|
||||
retentionPeriod: '1 year',
|
||||
sourceUrl: 'www.xy,z.com',
|
||||
entityType: 'Table',
|
||||
certification: 'Certification.Gold',
|
||||
};
|
||||
};
|
||||
|
||||
@ -488,6 +491,7 @@ export const createStoredProcedureRowDetails = () => {
|
||||
entityType: 'Stored Procedure',
|
||||
retentionPeriod: '1 year',
|
||||
sourceUrl: 'www.xyz.com',
|
||||
certification: 'Certification.Gold',
|
||||
};
|
||||
};
|
||||
|
||||
@ -558,6 +562,7 @@ export const fillRowDetails = async (
|
||||
parent: string;
|
||||
};
|
||||
tier: string;
|
||||
certification: string;
|
||||
retentionPeriod?: string;
|
||||
sourceUrl?: string;
|
||||
domains: {
|
||||
@ -622,6 +627,16 @@ export const fillRowDetails = async (
|
||||
|
||||
await page.click(`[data-testid="radio-btn-${row.tier}"]`);
|
||||
|
||||
await page
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowRight', { delay: 100 });
|
||||
await page
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('Enter', { delay: 100 });
|
||||
|
||||
await page.click(`[data-testid="radio-btn-${row.certification}"]`);
|
||||
await page.getByTestId('update-certification').click();
|
||||
|
||||
await page
|
||||
.locator('.InovuaReactDataGrid__cell--cell-active')
|
||||
.press('ArrowRight');
|
||||
@ -887,7 +902,7 @@ export const fillRecursiveColumnDetails = async (
|
||||
.press('ArrowRight', { delay: 100 });
|
||||
await fillGlossaryTermDetails(page, row.glossary);
|
||||
|
||||
await pressKeyXTimes(page, 6, 'ArrowRight');
|
||||
await pressKeyXTimes(page, 7, 'ArrowRight');
|
||||
|
||||
await fillEntityTypeDetails(page, row.entityType);
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 26 KiB |
@ -37,7 +37,10 @@ import {
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { getEntityDetailsPath } from '../../../utils/RouterUtils';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils';
|
||||
import { updateTierTag } from '../../../utils/TagsUtils';
|
||||
import {
|
||||
updateCertificationTag,
|
||||
updateTierTag,
|
||||
} from '../../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import { withActivityFeed } from '../../AppRouter/withActivityFeed';
|
||||
import { AlignRightIconButton } from '../../common/IconButtons/EditIconButton';
|
||||
@ -234,6 +237,20 @@ const APIEndpointDetails: React.FC<APIEndpointDetailsProps> = ({
|
||||
const toggleTabExpanded = () => {
|
||||
setIsTabExpanded(!isTabExpanded);
|
||||
};
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (apiEndpointDetails) {
|
||||
const certificationTag = updateCertificationTag(newCertification);
|
||||
const updatedApiEndpointDetails: APIEndpoint = {
|
||||
...apiEndpointDetails,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onApiEndpointUpdate(updatedApiEndpointDetails, 'certification');
|
||||
}
|
||||
},
|
||||
[apiEndpointDetails, onApiEndpointUpdate]
|
||||
);
|
||||
|
||||
const isExpandViewSupported = useMemo(
|
||||
() => checkIfExpandViewSupported(tabs[0], activeTab, PageType.APIEndpoint),
|
||||
@ -260,6 +277,7 @@ const APIEndpointDetails: React.FC<APIEndpointDetailsProps> = ({
|
||||
entityType={EntityType.API_ENDPOINT}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={apiEndpointPermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onFollowClick={followApiEndpoint}
|
||||
onOwnerUpdate={onOwnerUpdate}
|
||||
|
||||
@ -0,0 +1,213 @@
|
||||
/*
|
||||
* Copyright 2025 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 { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Popover, Radio, Space, Spin, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { t } from 'i18next';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ReactComponent as CertificationIcon } from '../../assets/svg/ic-certification.svg';
|
||||
import { Tag } from '../../generated/entity/classification/tag';
|
||||
import { getTags } from '../../rest/tagAPI';
|
||||
import { getEntityName } from '../../utils/EntityUtils';
|
||||
import { stringToHTML } from '../../utils/StringsUtils';
|
||||
import { getTagImageSrc } from '../../utils/TagsUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
import Loader from '../common/Loader/Loader';
|
||||
import { CertificationProps } from './Certification.interface';
|
||||
import './certification.less';
|
||||
const Certification = ({
|
||||
currentCertificate = '',
|
||||
children,
|
||||
onCertificationUpdate,
|
||||
popoverProps,
|
||||
onClose,
|
||||
}: CertificationProps) => {
|
||||
const popoverRef = useRef<any>(null);
|
||||
const [isLoadingCertificationData, setIsLoadingCertificationData] =
|
||||
useState<boolean>(false);
|
||||
const [certifications, setCertifications] = useState<Array<Tag>>([]);
|
||||
const [selectedCertification, setSelectedCertification] = useState<string>(
|
||||
currentCertificate ?? ''
|
||||
);
|
||||
const certificationCardData = useMemo(() => {
|
||||
return (
|
||||
<Radio.Group
|
||||
className="h-max-100 overflow-y-auto overflow-x-hidden"
|
||||
value={selectedCertification}>
|
||||
{certifications.map((certificate) => {
|
||||
const tagSrc = getTagImageSrc(certificate.style?.iconURL ?? '');
|
||||
const title = getEntityName(certificate);
|
||||
const { id, fullyQualifiedName, description } = certificate;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="certification-card-item cursor-pointer"
|
||||
key={id}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setSelectedCertification(fullyQualifiedName ?? '');
|
||||
}}>
|
||||
<Radio
|
||||
className="certification-radio-top-right"
|
||||
data-testid={`radio-btn-${fullyQualifiedName}`}
|
||||
value={fullyQualifiedName}
|
||||
/>
|
||||
<div className="certification-card-content">
|
||||
{tagSrc ? (
|
||||
<img alt={title} src={tagSrc} />
|
||||
) : (
|
||||
<div className="certification-icon">
|
||||
<CertificationIcon height={28} width={28} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-body">
|
||||
{title}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="m-b-0 font-regular text-xs text-grey-muted">
|
||||
{stringToHTML(description)}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Radio.Group>
|
||||
);
|
||||
}, [certifications, selectedCertification]);
|
||||
|
||||
const updateCertificationData = async (value?: string) => {
|
||||
setIsLoadingCertificationData(true);
|
||||
const certification = certifications.find(
|
||||
(cert) => cert.fullyQualifiedName === value
|
||||
);
|
||||
await onCertificationUpdate?.(certification);
|
||||
setIsLoadingCertificationData(false);
|
||||
popoverRef.current?.close();
|
||||
};
|
||||
const getCertificationData = async () => {
|
||||
setIsLoadingCertificationData(true);
|
||||
try {
|
||||
const { data } = await getTags({
|
||||
parent: 'Certification',
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
// Sort certifications with Gold, Silver, Bronze first
|
||||
const sortedData = [...data].sort((a, b) => {
|
||||
const order: Record<string, number> = {
|
||||
Gold: 0,
|
||||
Silver: 1,
|
||||
Bronze: 2,
|
||||
};
|
||||
|
||||
const aName = getEntityName(a);
|
||||
const bName = getEntityName(b);
|
||||
|
||||
const aOrder = order[aName] ?? 3;
|
||||
const bOrder = order[bName] ?? 3;
|
||||
|
||||
return aOrder - bOrder;
|
||||
});
|
||||
|
||||
setCertifications(sortedData);
|
||||
} catch (err) {
|
||||
showErrorToast(
|
||||
err as AxiosError,
|
||||
t('server.entity-fetch-error', {
|
||||
entity: t('label.certification-plural-lowercase'),
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsLoadingCertificationData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseCertification = async () => {
|
||||
popoverRef.current?.close();
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
const onOpenChange = (visible: boolean) => {
|
||||
if (visible) {
|
||||
getCertificationData();
|
||||
setSelectedCertification(currentCertificate);
|
||||
} else {
|
||||
setSelectedCertification('');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (popoverProps?.open && certifications.length === 0) {
|
||||
getCertificationData();
|
||||
}
|
||||
}, [popoverProps?.open]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
className="p-0"
|
||||
content={
|
||||
<Card
|
||||
bordered={false}
|
||||
className="certification-card"
|
||||
data-testid="certification-cards"
|
||||
title={
|
||||
<Space className="w-full justify-between">
|
||||
<div className="flex gap-2 items-center w-full">
|
||||
<CertificationIcon height={18} width={18} />
|
||||
<Typography.Text className="m-b-0 font-semibold text-sm">
|
||||
{t('label.edit-entity', { entity: t('label.certification') })}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text
|
||||
className="m-b-0 font-semibold text-primary text-sm cursor-pointer"
|
||||
data-testid="clear-certification"
|
||||
onClick={() => updateCertificationData()}>
|
||||
{t('label.clear')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
}>
|
||||
<Spin
|
||||
indicator={<Loader size="small" />}
|
||||
spinning={isLoadingCertificationData}>
|
||||
{certificationCardData}
|
||||
<div className="flex justify-end text-lg gap-2 mt-4">
|
||||
<Button
|
||||
data-testid="close-certification"
|
||||
type="default"
|
||||
onClick={handleCloseCertification}>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="update-certification"
|
||||
type="primary"
|
||||
onClick={() => updateCertificationData(selectedCertification)}>
|
||||
<CheckOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
</Spin>
|
||||
</Card>
|
||||
}
|
||||
overlayClassName="certification-card-popover"
|
||||
placement="bottomRight"
|
||||
ref={popoverRef}
|
||||
showArrow={false}
|
||||
trigger="click"
|
||||
onOpenChange={onOpenChange}
|
||||
{...popoverProps}>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default Certification;
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 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 { PopoverProps } from 'antd';
|
||||
import { ReactNode } from 'react';
|
||||
import { Tag } from '../../generated/entity/classification/tag';
|
||||
|
||||
export interface CertificationProps {
|
||||
permission: boolean;
|
||||
onCertificationUpdate?: (certification?: Tag) => Promise<void>;
|
||||
onClose?: () => void;
|
||||
currentCertificate?: string;
|
||||
popoverProps?: PopoverProps;
|
||||
children?: ReactNode;
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2025 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 (reference) '../../styles/variables.less';
|
||||
|
||||
.certification-value {
|
||||
min-width: 80%;
|
||||
|
||||
.certification-tag-with-name {
|
||||
width: fit-content;
|
||||
color: @grey-700;
|
||||
}
|
||||
}
|
||||
|
||||
.certification-card-popover {
|
||||
width: 350px;
|
||||
|
||||
.ant-popover-inner-content {
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.certification-card {
|
||||
.ant-card-head {
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0px;
|
||||
|
||||
.ant-radio-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.ant-collapse-item {
|
||||
width: 100%;
|
||||
border: 1px solid @grey-200;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.certification-radio-top-right {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.certification-card-item {
|
||||
position: relative;
|
||||
padding: 16px 16px 16px 12px;
|
||||
border: 1px solid @grey-200;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.certification-card-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-card-head-title {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.certification-icon {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
@ -38,7 +38,10 @@ import {
|
||||
import dashboardDetailsClassBase from '../../../utils/DashboardDetailsClassBase';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils';
|
||||
import { getEntityDetailsPath } from '../../../utils/RouterUtils';
|
||||
import { updateTierTag } from '../../../utils/TagsUtils';
|
||||
import {
|
||||
updateCertificationTag,
|
||||
updateTierTag,
|
||||
} from '../../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import { withActivityFeed } from '../../AppRouter/withActivityFeed';
|
||||
import { AlignRightIconButton } from '../../common/IconButtons/EditIconButton';
|
||||
@ -260,6 +263,21 @@ const DashboardDetails = ({
|
||||
viewAllPermission,
|
||||
onExtensionUpdate,
|
||||
]);
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (dashboardDetails) {
|
||||
const certificationTag: Dashboard['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedDashboardDetails = {
|
||||
...dashboardDetails,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onDashboardUpdate(updatedDashboardDetails, 'certification');
|
||||
}
|
||||
},
|
||||
[dashboardDetails, onDashboardUpdate]
|
||||
);
|
||||
|
||||
const toggleTabExpanded = () => {
|
||||
setIsTabExpanded(!isTabExpanded);
|
||||
@ -291,6 +309,7 @@ const DashboardDetails = ({
|
||||
entityType={EntityType.DASHBOARD}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={dashboardPermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={onUpdateDisplayName}
|
||||
onFollowClick={followDashboard}
|
||||
onOwnerUpdate={onOwnerUpdate}
|
||||
|
||||
@ -34,7 +34,10 @@ export interface DashboardDetailsProps {
|
||||
followDashboardHandler: () => Promise<void>;
|
||||
unFollowDashboardHandler: () => Promise<void>;
|
||||
versionHandler: () => void;
|
||||
onDashboardUpdate: (updatedDashboard: Dashboard) => Promise<void>;
|
||||
onDashboardUpdate: (
|
||||
updatedDashboard: Dashboard,
|
||||
key?: keyof Dashboard
|
||||
) => Promise<void>;
|
||||
handleToggleDelete: (version?: number) => void;
|
||||
onUpdateVote?: (data: QueryVote, id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { FEED_COUNT_INITIAL_DATA } from '../../../../constants/entity.constants';
|
||||
import { EntityTabs, EntityType } from '../../../../enums/entity.enum';
|
||||
import { Tag } from '../../../../generated/entity/classification/tag';
|
||||
import { DashboardDataModel } from '../../../../generated/entity/data/dashboardDataModel';
|
||||
import { PageType } from '../../../../generated/system/ui/page';
|
||||
import { useCustomPages } from '../../../../hooks/useCustomPages';
|
||||
@ -36,6 +37,7 @@ import {
|
||||
getEntityDetailsPath,
|
||||
getVersionPath,
|
||||
} from '../../../../utils/RouterUtils';
|
||||
import { updateCertificationTag } from '../../../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
|
||||
import { withActivityFeed } from '../../../AppRouter/withActivityFeed';
|
||||
import { AlignRightIconButton } from '../../../common/IconButtons/EditIconButton';
|
||||
@ -206,6 +208,21 @@ const DataModelDetails = ({
|
||||
),
|
||||
[tabs[0], activeTab]
|
||||
);
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (dataModelData) {
|
||||
const certificationTag: DashboardDataModel['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedTableDetails = {
|
||||
...dataModelData,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onUpdateDataModel(updatedTableDetails as DashboardDataModel);
|
||||
}
|
||||
},
|
||||
[onUpdateDataModel, dataModelData]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
@ -228,6 +245,7 @@ const DataModelDetails = ({
|
||||
entityType={EntityType.DASHBOARD_DATA_MODEL}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={dataModelPermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onFollowClick={handleFollowDataModel}
|
||||
onOwnerUpdate={handleUpdateOwner}
|
||||
|
||||
@ -15,7 +15,7 @@ import { Button, Col, Divider, Row, Space, Tooltip, Typography } from 'antd';
|
||||
import ButtonGroup from 'antd/lib/button/button-group';
|
||||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
import { get, isEmpty, isUndefined } from 'lodash';
|
||||
import QueryString from 'qs';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -51,7 +51,6 @@ import { getActiveAnnouncement } from '../../../rest/feedsAPI';
|
||||
import { getDataQualityLineage } from '../../../rest/lineageAPI';
|
||||
import { getContainerByName } from '../../../rest/storageAPI';
|
||||
import {
|
||||
ExtraInfoLabel,
|
||||
getDataAssetsHeaderInfo,
|
||||
getEntityExtraInfoLength,
|
||||
isDataAssetsWithServiceField,
|
||||
@ -69,6 +68,7 @@ import { getEntityTypeFromServiceCategory } from '../../../utils/ServiceUtils';
|
||||
import tableClassBase from '../../../utils/TableClassBase';
|
||||
import { getTierTags } from '../../../utils/TableUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import Certification from '../../Certification/Certification.component';
|
||||
import CertificationTag from '../../common/CertificationTag/CertificationTag';
|
||||
import AnnouncementCard from '../../common/EntityPageInfos/AnnouncementCard/AnnouncementCard';
|
||||
import AnnouncementDrawer from '../../common/EntityPageInfos/AnnouncementDrawer/AnnouncementDrawer';
|
||||
@ -117,6 +117,7 @@ export const DataAssetsHeader = ({
|
||||
disableRunAgentsButton = true,
|
||||
afterTriggerAction,
|
||||
isAutoPilotWorkflowStatusLoading = false,
|
||||
onCertificationUpdate,
|
||||
}: DataAssetsHeaderProps) => {
|
||||
const { serviceCategory } = useParams<{ serviceCategory: ServiceCategory }>();
|
||||
const { currentUser } = useApplicationStore();
|
||||
@ -130,6 +131,7 @@ export const DataAssetsHeader = ({
|
||||
const [isFollowingLoading, setIsFollowingLoading] = useState(false);
|
||||
const history = useHistory();
|
||||
const [isAutoPilotTriggering, setIsAutoPilotTriggering] = useState(false);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
const serviceType = get(dataAsset, 'serviceType', '');
|
||||
|
||||
@ -359,17 +361,24 @@ export const DataAssetsHeader = ({
|
||||
setIsFollowingLoading(false);
|
||||
}, [onFollowClick]);
|
||||
|
||||
const { editDomainPermission, editOwnerPermission, editTierPermission } =
|
||||
useMemo(
|
||||
() => ({
|
||||
editDomainPermission: permissions.EditAll && !dataAsset.deleted,
|
||||
editOwnerPermission:
|
||||
(permissions.EditAll || permissions.EditOwners) && !dataAsset.deleted,
|
||||
editTierPermission:
|
||||
(permissions.EditAll || permissions.EditTier) && !dataAsset.deleted,
|
||||
}),
|
||||
[permissions, dataAsset]
|
||||
);
|
||||
const {
|
||||
editDomainPermission,
|
||||
editOwnerPermission,
|
||||
editTierPermission,
|
||||
editCertificationPermission,
|
||||
} = useMemo(
|
||||
() => ({
|
||||
editDomainPermission: permissions.EditAll && !dataAsset.deleted,
|
||||
editOwnerPermission:
|
||||
(permissions.EditAll || permissions.EditOwners) && !dataAsset.deleted,
|
||||
editTierPermission:
|
||||
(permissions.EditAll || permissions.EditTier) && !dataAsset.deleted,
|
||||
editCertificationPermission:
|
||||
(permissions.EditAll || permissions.EditCertification) &&
|
||||
!dataAsset.deleted,
|
||||
}),
|
||||
[permissions, dataAsset]
|
||||
);
|
||||
|
||||
const tierSuggestionRender = useMemo(() => {
|
||||
if (entityType === EntityType.TABLE) {
|
||||
@ -701,21 +710,58 @@ export const DataAssetsHeader = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider className="self-center vertical-divider" type="vertical" />
|
||||
<ExtraInfoLabel
|
||||
dataTestId="certification-label"
|
||||
label={t('label.certification')}
|
||||
value={
|
||||
(dataAsset as Table).certification ? (
|
||||
<CertificationTag
|
||||
showName
|
||||
certification={(dataAsset as Table).certification!}
|
||||
/>
|
||||
) : (
|
||||
t('label.no-entity', { entity: t('label.certification') })
|
||||
)
|
||||
}
|
||||
/>
|
||||
{isUndefined(serviceCategory) && (
|
||||
<>
|
||||
<Divider
|
||||
className="self-center vertical-divider"
|
||||
type="vertical"
|
||||
/>
|
||||
<Certification
|
||||
currentCertificate={
|
||||
'certification' in dataAsset
|
||||
? dataAsset.certification?.tagLabel?.tagFQN
|
||||
: undefined
|
||||
}
|
||||
permission={false}
|
||||
onCertificationUpdate={onCertificationUpdate}>
|
||||
<div className="d-flex align-start extra-info-container">
|
||||
<Typography.Text
|
||||
className="whitespace-nowrap text-sm d-flex flex-col gap-2"
|
||||
data-testid="certification-label">
|
||||
<div className="flex gap-2">
|
||||
<span className="extra-info-label-heading">
|
||||
{t('label.certification')}
|
||||
</span>
|
||||
|
||||
{editCertificationPermission && (
|
||||
<EditIconButton
|
||||
newLook
|
||||
data-testid="edit-certification"
|
||||
size="small"
|
||||
title={t('label.edit-entity', {
|
||||
entity: t('label.certification'),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="font-medium certification-value">
|
||||
{(dataAsset as Table).certification ? (
|
||||
<CertificationTag
|
||||
showName
|
||||
certification={(dataAsset as Table).certification!}
|
||||
/>
|
||||
) : (
|
||||
t('label.no-entity', {
|
||||
entity: t('label.certification'),
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Certification>
|
||||
</>
|
||||
)}
|
||||
|
||||
{extraInfo}
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
@ -139,6 +139,7 @@ export type DataAssetsHeaderProps = {
|
||||
disableRunAgentsButton?: boolean;
|
||||
afterTriggerAction?: VoidFunction;
|
||||
isAutoPilotWorkflowStatusLoading?: boolean;
|
||||
onCertificationUpdate?: (certificate?: Tag) => Promise<void>;
|
||||
} & (
|
||||
| DataAssetTable
|
||||
| DataAssetTopic
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
*/
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { AUTO_PILOT_APP_NAME } from '../../../constants/Applications.constant';
|
||||
import { EntityType } from '../../../enums/entity.enum';
|
||||
import { ServiceCategory } from '../../../enums/service.enum';
|
||||
@ -318,7 +319,7 @@ describe('DataAssetsHeader component', () => {
|
||||
expect(screen.queryByTestId('source-url-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should always render certification', () => {
|
||||
it('should render certification only when serviceCategory is undefined', () => {
|
||||
const mockCertification: AssetCertification = {
|
||||
tagLabel: {
|
||||
tagFQN: 'Certification.Bronze',
|
||||
@ -337,7 +338,13 @@ describe('DataAssetsHeader component', () => {
|
||||
expiryDate: 1735406645688,
|
||||
};
|
||||
|
||||
// First test with certification
|
||||
// Mock useParams to return undefined serviceCategory
|
||||
const useParamsMock = useParams as jest.Mock;
|
||||
useParamsMock.mockReturnValue({
|
||||
serviceCategory: undefined,
|
||||
});
|
||||
|
||||
// Test with certification when serviceCategory is undefined
|
||||
const { unmount } = render(
|
||||
<DataAssetsHeader
|
||||
{...mockProps}
|
||||
@ -357,12 +364,53 @@ describe('DataAssetsHeader component', () => {
|
||||
// Clean up the first render before rendering again
|
||||
unmount();
|
||||
|
||||
// Second test without certification
|
||||
// Test without certification when serviceCategory is undefined
|
||||
render(<DataAssetsHeader {...mockProps} />);
|
||||
|
||||
expect(screen.getByTestId('certification-label')).toContainHTML(
|
||||
'label.no-entity'
|
||||
);
|
||||
|
||||
// Reset the mock to original value
|
||||
useParamsMock.mockReturnValue({
|
||||
serviceCategory: ServiceCategory.DATABASE_SERVICES,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render certification when serviceCategory has a value', () => {
|
||||
const mockCertification: AssetCertification = {
|
||||
tagLabel: {
|
||||
tagFQN: 'Certification.Bronze',
|
||||
name: 'Bronze',
|
||||
displayName: 'Bronze_Medal',
|
||||
description: 'Bronze certified Data Asset test',
|
||||
style: {
|
||||
color: '#C08329',
|
||||
iconURL: 'BronzeCertification.svg',
|
||||
},
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
appliedDate: 1732814645688,
|
||||
expiryDate: 1735406645688,
|
||||
};
|
||||
|
||||
// serviceCategory is already set to DATABASE_SERVICES by default mock
|
||||
render(
|
||||
<DataAssetsHeader
|
||||
{...mockProps}
|
||||
dataAsset={{
|
||||
...mockProps.dataAsset,
|
||||
certification: mockCertification,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
// Certification should not be rendered when serviceCategory has a value
|
||||
expect(screen.queryByText('label.certification')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('CertificationTag')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('certification-label')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should trigger the AutoPilot application when the button is clicked', () => {
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
import Icon from '@ant-design/icons';
|
||||
import { Button, Checkbox, Col, Row, Space, Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { isObject, isString, startCase, uniqueId } from 'lodash';
|
||||
import { isEmpty, isObject, isString, startCase, uniqueId } from 'lodash';
|
||||
import { ExtraInfo } from 'Models';
|
||||
import React, { forwardRef, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -286,7 +286,9 @@ const ExploreSearchCard: React.FC<ExploreSearchCardProps> = forwardRef<
|
||||
</Typography.Text>
|
||||
</Link>
|
||||
|
||||
{(source as Table)?.certification && (
|
||||
{!isEmpty(
|
||||
(source as Table)?.certification?.tagLabel?.tagFQN
|
||||
) && (
|
||||
<div className="p-l-sm">
|
||||
<CertificationTag
|
||||
certification={
|
||||
|
||||
@ -37,7 +37,10 @@ import {
|
||||
} from '../../../utils/CustomizePage/CustomizePageUtils';
|
||||
import metricDetailsClassBase from '../../../utils/MetricEntityUtils/MetricDetailsClassBase';
|
||||
import { getEntityDetailsPath } from '../../../utils/RouterUtils';
|
||||
import { updateTierTag } from '../../../utils/TagsUtils';
|
||||
import {
|
||||
updateCertificationTag,
|
||||
updateTierTag,
|
||||
} from '../../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import { withActivityFeed } from '../../AppRouter/withActivityFeed';
|
||||
import { AlignRightIconButton } from '../../common/IconButtons/EditIconButton';
|
||||
@ -98,6 +101,19 @@ const MetricDetails: React.FC<MetricDetailsProps> = ({
|
||||
await onMetricUpdate(updatedData, 'displayName');
|
||||
};
|
||||
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
const certificationTag = updateCertificationTag(newCertification);
|
||||
const updatedData = {
|
||||
...metricDetails,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onMetricUpdate(updatedData, 'certification');
|
||||
},
|
||||
[metricDetails, onMetricUpdate]
|
||||
);
|
||||
|
||||
const handleRestoreMetric = async () => {
|
||||
try {
|
||||
const { version: newVersion } = await restoreMetric(metricDetails.id);
|
||||
@ -248,6 +264,7 @@ const MetricDetails: React.FC<MetricDetailsProps> = ({
|
||||
entityType={EntityType.METRIC}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={metricPermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onFollowClick={followMetric}
|
||||
onMetricUpdate={onMetricUpdate}
|
||||
|
||||
@ -175,6 +175,7 @@ const mockProp = {
|
||||
versionHandler: jest.fn(),
|
||||
handleToggleDelete: jest.fn(),
|
||||
onUpdateVote: jest.fn(),
|
||||
onMlModelUpdateCertification: jest.fn(),
|
||||
};
|
||||
|
||||
const mockParams = {
|
||||
|
||||
@ -43,7 +43,10 @@ import mlModelDetailsClassBase from '../../../utils/MlModel/MlModelClassBase';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils';
|
||||
import { getEntityDetailsPath } from '../../../utils/RouterUtils';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils';
|
||||
import { updateTierTag } from '../../../utils/TagsUtils';
|
||||
import {
|
||||
updateCertificationTag,
|
||||
updateTierTag,
|
||||
} from '../../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import { withActivityFeed } from '../../AppRouter/withActivityFeed';
|
||||
import { AlignRightIconButton } from '../../common/IconButtons/EditIconButton';
|
||||
@ -65,6 +68,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
|
||||
versionHandler,
|
||||
handleToggleDelete,
|
||||
onMlModelUpdate,
|
||||
onMlModelUpdateCertification,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentUser } = useApplicationStore();
|
||||
@ -358,6 +362,24 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
|
||||
fetchMlModel,
|
||||
customizedPage?.tabs,
|
||||
]);
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (mlModelDetail) {
|
||||
const certificationTag: Mlmodel['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedMlModelDetails = {
|
||||
...mlModelDetail,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onMlModelUpdateCertification(
|
||||
updatedMlModelDetails,
|
||||
'certification'
|
||||
);
|
||||
}
|
||||
},
|
||||
[mlModelDetail, onMlModelUpdateCertification]
|
||||
);
|
||||
|
||||
const toggleTabExpanded = () => {
|
||||
setIsTabExpanded(!isTabExpanded);
|
||||
@ -387,6 +409,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
|
||||
entityType={EntityType.MLMODEL}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={mlModelPermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onFollowClick={followMlModel}
|
||||
onOwnerUpdate={onOwnerUpdate}
|
||||
|
||||
@ -27,4 +27,8 @@ export interface MlModelDetailProp extends HTMLAttributes<HTMLDivElement> {
|
||||
handleToggleDelete: (version?: number) => void;
|
||||
onUpdateVote: (data: QueryVote, id: string) => Promise<void>;
|
||||
onMlModelUpdate: (data: Mlmodel) => Promise<void>;
|
||||
onMlModelUpdateCertification: (
|
||||
data: Mlmodel,
|
||||
key: keyof Mlmodel
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
@ -40,7 +40,11 @@ import { DEFAULT_ENTITY_PERMISSION } from '../../../utils/PermissionsUtils';
|
||||
import pipelineClassBase from '../../../utils/PipelineClassBase';
|
||||
import { getEntityDetailsPath } from '../../../utils/RouterUtils';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils';
|
||||
import { createTagObject, updateTierTag } from '../../../utils/TagsUtils';
|
||||
import {
|
||||
createTagObject,
|
||||
updateCertificationTag,
|
||||
updateTierTag,
|
||||
} from '../../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import { withActivityFeed } from '../../AppRouter/withActivityFeed';
|
||||
import { AlignRightIconButton } from '../../common/IconButtons/EditIconButton';
|
||||
@ -65,6 +69,7 @@ const PipelineDetails = ({
|
||||
onUpdateVote,
|
||||
onExtensionUpdate,
|
||||
handleToggleDelete,
|
||||
onPipelineUpdate,
|
||||
}: PipeLineDetailsProp) => {
|
||||
const history = useHistory();
|
||||
const { tab } = useParams<{ tab: EntityTabs }>();
|
||||
@ -309,6 +314,21 @@ const PipelineDetails = ({
|
||||
setIsTabExpanded(!isTabExpanded);
|
||||
};
|
||||
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (pipelineDetails && updatePipelineDetailsState) {
|
||||
const certificationTag: Pipeline['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedPipelineDetails = {
|
||||
...pipelineDetails,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onPipelineUpdate(updatedPipelineDetails, 'certification');
|
||||
}
|
||||
},
|
||||
[pipelineDetails, onPipelineUpdate]
|
||||
);
|
||||
const isExpandViewSupported = useMemo(
|
||||
() => checkIfExpandViewSupported(tabs[0], tab, PageType.Pipeline),
|
||||
[tabs[0], tab]
|
||||
@ -334,6 +354,7 @@ const PipelineDetails = ({
|
||||
entityType={EntityType.PIPELINE}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={pipelinePermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onFollowClick={followPipeline}
|
||||
onOwnerUpdate={onOwnerUpdate}
|
||||
|
||||
@ -32,4 +32,8 @@ export interface PipeLineDetailsProp {
|
||||
onExtensionUpdate: (updatedPipeline: Pipeline) => Promise<void>;
|
||||
handleToggleDelete: (version?: number) => void;
|
||||
onUpdateVote: (data: QueryVote, id: string) => Promise<void>;
|
||||
onPipelineUpdate: (
|
||||
updatedPipeline: Pipeline,
|
||||
key?: keyof Pipeline
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
@ -68,6 +68,7 @@ const PipelineDetailsProps: PipeLineDetailsProp = {
|
||||
onExtensionUpdate: jest.fn(),
|
||||
handleToggleDelete: jest.fn(),
|
||||
onUpdateVote: jest.fn(),
|
||||
onPipelineUpdate: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock(
|
||||
|
||||
@ -44,7 +44,11 @@ import {
|
||||
} from '../../../utils/EntityUtils';
|
||||
import { getEntityDetailsPath } from '../../../utils/RouterUtils';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../../utils/TableUtils';
|
||||
import { createTagObject, updateTierTag } from '../../../utils/TagsUtils';
|
||||
import {
|
||||
createTagObject,
|
||||
updateCertificationTag,
|
||||
updateTierTag,
|
||||
} from '../../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
|
||||
import topicClassBase from '../../../utils/TopicClassBase';
|
||||
import { ActivityFeedTab } from '../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component';
|
||||
@ -372,6 +376,21 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
viewSampleDataPermission,
|
||||
viewAllPermission,
|
||||
]);
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (topicDetails) {
|
||||
const certificationTag: Topic['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedTopicDetails = {
|
||||
...topicDetails,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onTopicUpdate(updatedTopicDetails, 'certification');
|
||||
}
|
||||
},
|
||||
[topicDetails, onTopicUpdate]
|
||||
);
|
||||
|
||||
const toggleTabExpanded = () => {
|
||||
setIsTabExpanded(!isTabExpanded);
|
||||
@ -401,6 +420,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
|
||||
entityType={EntityType.TOPIC}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={topicPermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onFollowClick={followTopic}
|
||||
onOwnerUpdate={onOwnerUpdate}
|
||||
|
||||
@ -10,10 +10,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Tag, Tooltip } from 'antd';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ReactComponent as CertificationIcon } from '../../../assets/svg/ic-certification.svg';
|
||||
import { AssetCertification } from '../../../generated/entity/data/table';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { getClassificationTagPath } from '../../../utils/RouterUtils';
|
||||
@ -27,58 +28,65 @@ const CertificationTag = ({
|
||||
certification: AssetCertification;
|
||||
showName?: boolean;
|
||||
}) => {
|
||||
if (certification.tagLabel.style?.iconURL) {
|
||||
const imageItem = useMemo(() => {
|
||||
if (certification.tagLabel.style?.iconURL) {
|
||||
const name = getEntityName(certification.tagLabel);
|
||||
const tagSrc = getTagImageSrc(certification.tagLabel.style.iconURL);
|
||||
|
||||
return (
|
||||
<img
|
||||
alt={`certification: ${name}`}
|
||||
className="certification-img"
|
||||
src={tagSrc}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const iconSize = showName ? 14 : 20;
|
||||
|
||||
return <CertificationIcon height={iconSize} width={iconSize} />;
|
||||
}, [certification.tagLabel.style?.iconURL, showName]);
|
||||
|
||||
const certificationRender = useMemo(() => {
|
||||
const name = getEntityName(certification.tagLabel);
|
||||
const actualName = certification.tagLabel.name ?? '';
|
||||
const tagSrc = getTagImageSrc(certification.tagLabel.style.iconURL);
|
||||
const tagLink = getClassificationTagPath(certification.tagLabel.tagFQN);
|
||||
|
||||
const tagStyle = showName
|
||||
? {
|
||||
backgroundColor: certification.tagLabel.style?.color
|
||||
? certification.tagLabel.style?.color + '33'
|
||||
: '#f8f8f8',
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={getTagTooltip(name, certification.tagLabel.description)}
|
||||
trigger="hover">
|
||||
<Link
|
||||
className={classNames({
|
||||
'certification-tag-with-name d-flex items-center gap-1': showName,
|
||||
className={classNames('d-flex items-center', {
|
||||
'certification-tag-with-name gap-1': showName,
|
||||
})}
|
||||
data-testid={`certification-${certification.tagLabel.tagFQN}`}
|
||||
style={
|
||||
showName
|
||||
? { backgroundColor: certification.tagLabel.style?.color + '33' } // to decrease opacity of the background color by 80%
|
||||
: {}
|
||||
}
|
||||
style={tagStyle}
|
||||
to={tagLink}>
|
||||
<img
|
||||
alt={`certification: ${name}`}
|
||||
className="certification-img"
|
||||
src={tagSrc}
|
||||
/>
|
||||
{imageItem}
|
||||
{showName && (
|
||||
<span
|
||||
className={classNames('text-sm font-medium', {
|
||||
<Typography.Text
|
||||
className={classNames('text-sm font-medium certification-text', {
|
||||
[`${actualName.toLowerCase()}`]: Boolean(actualName),
|
||||
})}>
|
||||
})}
|
||||
ellipsis={{ tooltip: true }}>
|
||||
{name}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
)}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}, [certification, imageItem]);
|
||||
|
||||
return (
|
||||
<Tag
|
||||
className="certification-tag"
|
||||
data-testid={`certification-${certification.tagLabel.tagFQN}`}
|
||||
style={{
|
||||
borderColor: certification.tagLabel.style?.color,
|
||||
backgroundColor: certification.tagLabel.style?.color
|
||||
? `${certification.tagLabel.style.color}33`
|
||||
: undefined, // Assuming 33 is the hex transparency for lighter shade
|
||||
}}>
|
||||
{getEntityName(certification.tagLabel)}
|
||||
</Tag>
|
||||
);
|
||||
return certificationRender;
|
||||
};
|
||||
|
||||
export default CertificationTag;
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
@import (reference) '../../../styles/variables.less';
|
||||
|
||||
.certification-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -32,18 +33,27 @@
|
||||
|
||||
.certification-tag-with-name {
|
||||
border-radius: 16px;
|
||||
padding: 2px 8px 2px 10px;
|
||||
padding: 4px 10px;
|
||||
|
||||
.certification-img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
span.certification-text {
|
||||
color: @grey-700;
|
||||
font-size: 12px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
span.bronze {
|
||||
color: @red-16;
|
||||
}
|
||||
|
||||
span.silver {
|
||||
color: @grey-700;
|
||||
}
|
||||
|
||||
span.gold {
|
||||
color: @yellow-11;
|
||||
}
|
||||
|
||||
@ -600,6 +600,7 @@ export enum DataModelType {
|
||||
LookMlExplore = "LookMlExplore",
|
||||
LookMlView = "LookMlView",
|
||||
MetabaseDataModel = "MetabaseDataModel",
|
||||
MicroStrategyDataset = "MicroStrategyDataset",
|
||||
PowerBIDataFlow = "PowerBIDataFlow",
|
||||
PowerBIDataModel = "PowerBIDataModel",
|
||||
QlikDataModel = "QlikDataModel",
|
||||
|
||||
@ -156,6 +156,7 @@ export enum Operation {
|
||||
DeleteTestCaseFailedRowsSample = "DeleteTestCaseFailedRowsSample",
|
||||
Deploy = "Deploy",
|
||||
EditAll = "EditAll",
|
||||
EditCertification = "EditCertification",
|
||||
EditCustomFields = "EditCustomFields",
|
||||
EditDataProfile = "EditDataProfile",
|
||||
EditDescription = "EditDescription",
|
||||
|
||||
@ -38,6 +38,7 @@ export enum Operation {
|
||||
DeleteTestCaseFailedRowsSample = "DeleteTestCaseFailedRowsSample",
|
||||
Deploy = "Deploy",
|
||||
EditAll = "EditAll",
|
||||
EditCertification = "EditCertification",
|
||||
EditCustomFields = "EditCustomFields",
|
||||
EditDataProfile = "EditDataProfile",
|
||||
EditDescription = "EditDescription",
|
||||
|
||||
@ -80,6 +80,7 @@ export enum Operation {
|
||||
DeleteTestCaseFailedRowsSample = "DeleteTestCaseFailedRowsSample",
|
||||
Deploy = "Deploy",
|
||||
EditAll = "EditAll",
|
||||
EditCertification = "EditCertification",
|
||||
EditCustomFields = "EditCustomFields",
|
||||
EditDataProfile = "EditDataProfile",
|
||||
EditDescription = "EditDescription",
|
||||
|
||||
@ -63,6 +63,7 @@ export enum Operation {
|
||||
DeleteTestCaseFailedRowsSample = "DeleteTestCaseFailedRowsSample",
|
||||
Deploy = "Deploy",
|
||||
EditAll = "EditAll",
|
||||
EditCertification = "EditCertification",
|
||||
EditCustomFields = "EditCustomFields",
|
||||
EditDataProfile = "EditDataProfile",
|
||||
EditDescription = "EditDescription",
|
||||
|
||||
@ -306,6 +306,7 @@ export enum Operation {
|
||||
DeleteTestCaseFailedRowsSample = "DeleteTestCaseFailedRowsSample",
|
||||
Deploy = "Deploy",
|
||||
EditAll = "EditAll",
|
||||
EditCertification = "EditCertification",
|
||||
EditCustomFields = "EditCustomFields",
|
||||
EditDataProfile = "EditDataProfile",
|
||||
EditDescription = "EditDescription",
|
||||
|
||||
@ -1,15 +1,3 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
/**
|
||||
* This schema defines the `Database Service` is a service such as MySQL, BigQuery,
|
||||
* Redshift, Postgres, or Snowflake. Alternative terms such as Database Cluster, Database
|
||||
|
||||
@ -1,15 +1,3 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
/**
|
||||
* Ingestion Pipeline Config is used to set up a DAG and deploy. This entity is used to
|
||||
* setup metadata/quality pipelines on Apache Airflow.
|
||||
|
||||
@ -1,15 +1,3 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
/**
|
||||
* This schema defines the Pipeline Service entity, such as Airflow and Prefect.
|
||||
*/
|
||||
|
||||
@ -1,15 +1,3 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
/**
|
||||
* Defines a background job that is triggered on insertion of new record in background_jobs
|
||||
* table.
|
||||
|
||||
@ -73,7 +73,7 @@ import entityUtilClassBase from '../../utils/EntityUtilClassBase';
|
||||
import { getEntityName } from '../../utils/EntityUtils';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
|
||||
import { getEntityDetailsPath, getVersionPath } from '../../utils/RouterUtils';
|
||||
import { updateTierTag } from '../../utils/TagsUtils';
|
||||
import { updateCertificationTag, updateTierTag } from '../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
|
||||
const APICollectionPage: FunctionComponent = () => {
|
||||
@ -443,6 +443,21 @@ const APICollectionPage: FunctionComponent = () => {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (apiCollection) {
|
||||
const certificationTag: APICollection['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedTableDetails = {
|
||||
...apiCollection,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await handleAPICollectionUpdate(updatedTableDetails as APICollection);
|
||||
}
|
||||
},
|
||||
[handleAPICollectionUpdate, apiCollection]
|
||||
);
|
||||
|
||||
const toggleTabExpanded = () => {
|
||||
setIsTabExpanded(!isTabExpanded);
|
||||
@ -502,6 +517,7 @@ const APICollectionPage: FunctionComponent = () => {
|
||||
entityType={EntityType.API_COLLECTION}
|
||||
extraDropdownContent={extraDropdownContent}
|
||||
permissions={apiCollectionPermission}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onOwnerUpdate={handleUpdateOwner}
|
||||
onRestoreDataAsset={handleRestoreAPICollection}
|
||||
|
||||
@ -73,7 +73,7 @@ import {
|
||||
import { getEntityName } from '../../utils/EntityUtils';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
|
||||
import { getEntityDetailsPath, getVersionPath } from '../../utils/RouterUtils';
|
||||
import { updateTierTag } from '../../utils/TagsUtils';
|
||||
import { updateCertificationTag, updateTierTag } from '../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
|
||||
const ContainerPage = () => {
|
||||
@ -441,6 +441,27 @@ const ContainerPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onContainerUpdateCertification = async (
|
||||
updatedContainer: Container,
|
||||
key?: keyof Container
|
||||
) => {
|
||||
try {
|
||||
const response = await handleUpdateContainerData(updatedContainer);
|
||||
setContainerData((previous) => {
|
||||
if (!previous) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
return {
|
||||
...previous,
|
||||
version: response.version,
|
||||
...(key ? { [key]: response[key] } : response),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
const tabs = useMemo(() => {
|
||||
const tabLabelMap = getTabLabelMapFromTabs(customizedPage?.tabs);
|
||||
|
||||
@ -517,6 +538,24 @@ const ContainerPage = () => {
|
||||
[tabs[0], tab]
|
||||
);
|
||||
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (containerData) {
|
||||
const certificationTag: Container['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedTableDetails = {
|
||||
...containerData,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onContainerUpdateCertification(
|
||||
updatedTableDetails,
|
||||
'certification'
|
||||
);
|
||||
}
|
||||
},
|
||||
[containerData, handleContainerUpdate]
|
||||
);
|
||||
// Rendering
|
||||
if (isLoading || loading) {
|
||||
return <Loader />;
|
||||
@ -562,6 +601,7 @@ const ContainerPage = () => {
|
||||
entityType={EntityType.CONTAINER}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={containerPermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onFollowClick={handleFollowContainer}
|
||||
onOwnerUpdate={handleUpdateOwner}
|
||||
|
||||
@ -217,8 +217,7 @@ const DataModelsPage = () => {
|
||||
try {
|
||||
const response = await handleUpdateDataModelData(updatedDataModel);
|
||||
|
||||
setDataModelData((prev) => ({
|
||||
...prev,
|
||||
setDataModelData(() => ({
|
||||
...response,
|
||||
...(key && { [key]: response[key] }),
|
||||
}));
|
||||
|
||||
@ -85,7 +85,7 @@ import {
|
||||
getVersionPath,
|
||||
} from '../../utils/RouterUtils';
|
||||
import { getTierTags } from '../../utils/TableUtils';
|
||||
import { updateTierTag } from '../../utils/TagsUtils';
|
||||
import { updateCertificationTag, updateTierTag } from '../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
|
||||
const DatabaseDetails: FunctionComponent = () => {
|
||||
@ -516,6 +516,21 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
const toggleTabExpanded = () => {
|
||||
setIsTabExpanded(!isTabExpanded);
|
||||
};
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (database) {
|
||||
const certificationTag: Database['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedTableDetails = {
|
||||
...database,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await settingsUpdateHandler(updatedTableDetails as Database);
|
||||
}
|
||||
},
|
||||
[settingsUpdateHandler, database]
|
||||
);
|
||||
|
||||
const isExpandViewSupported = useMemo(
|
||||
() => checkIfExpandViewSupported(tabs[0], activeTab, PageType.Database),
|
||||
@ -559,6 +574,7 @@ const DatabaseDetails: FunctionComponent = () => {
|
||||
extraDropdownContent={extraDropdownContent}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={databasePermission}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onFollowClick={handleFollowClick}
|
||||
onOwnerUpdate={handleUpdateOwner}
|
||||
|
||||
@ -83,7 +83,7 @@ import entityUtilClassBase from '../../utils/EntityUtilClassBase';
|
||||
import { getEntityName } from '../../utils/EntityUtils';
|
||||
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
|
||||
import { getEntityDetailsPath, getVersionPath } from '../../utils/RouterUtils';
|
||||
import { updateTierTag } from '../../utils/TagsUtils';
|
||||
import { updateCertificationTag, updateTierTag } from '../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
|
||||
const DatabaseSchemaPage: FunctionComponent = () => {
|
||||
@ -581,6 +581,22 @@ const DatabaseSchemaPage: FunctionComponent = () => {
|
||||
}
|
||||
}, [USERId, databaseSchemaId]);
|
||||
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (databaseSchema) {
|
||||
const certificationTag: DatabaseSchema['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedTableDetails = {
|
||||
...databaseSchema,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await handleUpdateDatabaseSchema(updatedTableDetails as DatabaseSchema);
|
||||
}
|
||||
},
|
||||
[handleUpdateDatabaseSchema, databaseSchema]
|
||||
);
|
||||
|
||||
const handleFollowClick = useCallback(async () => {
|
||||
isFollowing ? await unFollowSchema() : await followSchema();
|
||||
}, [isFollowing, unFollowSchema, followSchema]);
|
||||
@ -634,6 +650,7 @@ const DatabaseSchemaPage: FunctionComponent = () => {
|
||||
entityType={EntityType.DATABASE_SCHEMA}
|
||||
extraDropdownContent={extraDropdownContent}
|
||||
permissions={databaseSchemaPermission}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleUpdateDisplayName}
|
||||
onFollowClick={handleFollowClick}
|
||||
onOwnerUpdate={handleUpdateOwner}
|
||||
|
||||
@ -257,6 +257,23 @@ const MlModelPage = () => {
|
||||
},
|
||||
[saveUpdatedMlModelData]
|
||||
);
|
||||
const onMlModelUpdateCertification = async (
|
||||
updatedMlModel: Mlmodel,
|
||||
key?: keyof Mlmodel
|
||||
) => {
|
||||
try {
|
||||
const response = await saveUpdatedMlModelData(updatedMlModel);
|
||||
setMlModelDetail((previous) => {
|
||||
return {
|
||||
...previous,
|
||||
version: response.version,
|
||||
...(key ? { [key]: response[key] } : response),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchResourcePermission(mlModelFqn);
|
||||
@ -297,6 +314,7 @@ const MlModelPage = () => {
|
||||
updateMlModelDetailsState={updateMlModelDetailsState}
|
||||
versionHandler={versionHandler}
|
||||
onMlModelUpdate={handleMlModelUpdate}
|
||||
onMlModelUpdateCertification={onMlModelUpdateCertification}
|
||||
onUpdateVote={updateVote}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -198,6 +198,24 @@ const PipelineDetailsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onPipelineUpdate = async (
|
||||
updatedPipeline: Pipeline,
|
||||
key?: keyof Pipeline
|
||||
) => {
|
||||
try {
|
||||
const response = await saveUpdatedPipelineData(updatedPipeline);
|
||||
setPipelineDetails((previous) => {
|
||||
return {
|
||||
...previous,
|
||||
version: response.version,
|
||||
...(key ? { [key]: response[key] } : response),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
const settingsUpdateHandler = async (updatedPipeline: Pipeline) => {
|
||||
try {
|
||||
const res = await saveUpdatedPipelineData(updatedPipeline);
|
||||
@ -332,6 +350,7 @@ const PipelineDetailsPage = () => {
|
||||
updatePipelineDetailsState={updatePipelineDetailsState}
|
||||
versionHandler={versionHandler}
|
||||
onExtensionUpdate={handleExtensionUpdate}
|
||||
onPipelineUpdate={onPipelineUpdate}
|
||||
onUpdateVote={updateVote}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -64,7 +64,7 @@ import { getEntityDetailsPath, getVersionPath } from '../../utils/RouterUtils';
|
||||
import searchIndexClassBase from '../../utils/SearchIndexDetailsClassBase';
|
||||
import { defaultFields } from '../../utils/SearchIndexUtils';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
|
||||
import { updateTierTag } from '../../utils/TagsUtils';
|
||||
import { updateCertificationTag, updateTierTag } from '../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
|
||||
function SearchIndexDetailsPage() {
|
||||
@ -511,6 +511,22 @@ function SearchIndexDetailsPage() {
|
||||
setIsTabExpanded(!isTabExpanded);
|
||||
};
|
||||
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (searchIndexDetails) {
|
||||
const certificationTag: SearchIndex['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedTableDetails = {
|
||||
...searchIndexDetails,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onSearchIndexUpdate(updatedTableDetails, 'certification');
|
||||
}
|
||||
},
|
||||
[onSearchIndexUpdate, searchIndexDetails]
|
||||
);
|
||||
|
||||
const isExpandViewSupported = useMemo(
|
||||
() => checkIfExpandViewSupported(tabs[0], activeTab, PageType.SearchIndex),
|
||||
[tabs[0], activeTab]
|
||||
@ -554,6 +570,7 @@ function SearchIndexDetailsPage() {
|
||||
entityType={EntityType.SEARCH_INDEX}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={searchIndexPermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleDisplayNameUpdate}
|
||||
onFollowClick={handleFollowSearchIndex}
|
||||
onOwnerUpdate={handleUpdateOwner}
|
||||
|
||||
@ -69,7 +69,7 @@ import {
|
||||
STORED_PROCEDURE_DEFAULT_FIELDS,
|
||||
} from '../../utils/StoredProceduresUtils';
|
||||
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
|
||||
import { updateTierTag } from '../../utils/TagsUtils';
|
||||
import { updateCertificationTag, updateTierTag } from '../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
|
||||
const StoredProcedurePage = () => {
|
||||
@ -511,6 +511,24 @@ const StoredProcedurePage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (storedProcedure) {
|
||||
const certificationTag: StoredProcedure['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedStoredProcedureDetails = {
|
||||
...storedProcedure,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await handleStoreProcedureUpdate(
|
||||
updatedStoredProcedureDetails,
|
||||
'certification'
|
||||
);
|
||||
}
|
||||
},
|
||||
[storedProcedure, handleStoreProcedureUpdate]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (decodedStoredProcedureFQN) {
|
||||
fetchResourcePermission();
|
||||
@ -559,6 +577,7 @@ const StoredProcedurePage = () => {
|
||||
entityType={EntityType.STORED_PROCEDURE}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={storedProcedurePermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleDisplayNameUpdate}
|
||||
onFollowClick={handleFollow}
|
||||
onOwnerUpdate={handleUpdateOwner}
|
||||
|
||||
@ -98,7 +98,7 @@ import {
|
||||
getTierTags,
|
||||
updateColumnInNestedStructure,
|
||||
} from '../../utils/TableUtils';
|
||||
import { updateTierTag } from '../../utils/TagsUtils';
|
||||
import { updateCertificationTag, updateTierTag } from '../../utils/TagsUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
import { useTestCaseStore } from '../IncidentManager/IncidentManagerDetailPage/useTestCase.store';
|
||||
|
||||
@ -565,6 +565,21 @@ const TableDetailsPageV1: React.FC = () => {
|
||||
[tableDetails, onTableUpdate, tableTags]
|
||||
);
|
||||
|
||||
const onCertificationUpdate = useCallback(
|
||||
async (newCertification?: Tag) => {
|
||||
if (tableDetails) {
|
||||
const certificationTag: Table['certification'] =
|
||||
updateCertificationTag(newCertification);
|
||||
const updatedTableDetails = {
|
||||
...tableDetails,
|
||||
certification: certificationTag,
|
||||
};
|
||||
|
||||
await onTableUpdate(updatedTableDetails, 'certification');
|
||||
}
|
||||
},
|
||||
[tableDetails, onTableUpdate]
|
||||
);
|
||||
const handleToggleDelete = (version?: number) => {
|
||||
setTableDetails((prev) => {
|
||||
if (!prev) {
|
||||
@ -807,6 +822,7 @@ const TableDetailsPageV1: React.FC = () => {
|
||||
extraDropdownContent={extraDropdownContent}
|
||||
openTaskCount={feedCount.openTaskCount}
|
||||
permissions={tablePermissions}
|
||||
onCertificationUpdate={onCertificationUpdate}
|
||||
onDisplayNameUpdate={handleDisplayNameUpdate}
|
||||
onFollowClick={handleFollowTable}
|
||||
onOwnerUpdate={handleUpdateOwner}
|
||||
|
||||
@ -268,6 +268,13 @@
|
||||
.h-max-56 {
|
||||
max-height: 14rem /* 224px */;
|
||||
}
|
||||
.h-max-80 {
|
||||
max-height: 20rem /* 320px */;
|
||||
}
|
||||
.h-max-100 {
|
||||
max-height: 25rem /* 400px */;
|
||||
}
|
||||
|
||||
.h-min-0 {
|
||||
min-height: 0px;
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import Select, { DefaultOptionType } from 'antd/lib/select';
|
||||
import { t } from 'i18next';
|
||||
import { toString } from 'lodash';
|
||||
import React, { ReactNode } from 'react';
|
||||
import Certification from '../../components/Certification/Certification.component';
|
||||
import TreeAsyncSelectList from '../../components/common/AsyncSelectList/TreeAsyncSelectList';
|
||||
import DomainSelectableList from '../../components/common/DomainSelectableList/DomainSelectableList.component';
|
||||
import InlineEdit from '../../components/common/InlineEdit/InlineEdit.component';
|
||||
@ -206,6 +207,31 @@ class CSVUtilsClassBase {
|
||||
</TierCard>
|
||||
);
|
||||
};
|
||||
|
||||
case 'certification':
|
||||
return ({ value, ...props }) => {
|
||||
const handleChange = async (tag?: Tag) => {
|
||||
props.onChange(tag?.fullyQualifiedName);
|
||||
|
||||
setTimeout(() => {
|
||||
props.onComplete(tag?.fullyQualifiedName);
|
||||
}, 1);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
props.onCancel();
|
||||
};
|
||||
|
||||
return (
|
||||
<Certification
|
||||
permission
|
||||
currentCertificate={value}
|
||||
popoverProps={{ open: true }}
|
||||
onCertificationUpdate={handleChange}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
case 'domain':
|
||||
return ({ value, ...props }) => {
|
||||
const handleChange = async (domain?: EntityReference) => {
|
||||
|
||||
@ -34,7 +34,11 @@ import { SearchIndex } from '../enums/search.enum';
|
||||
import { Classification } from '../generated/entity/classification/classification';
|
||||
import { Tag } from '../generated/entity/classification/tag';
|
||||
import { GlossaryTerm } from '../generated/entity/data/glossaryTerm';
|
||||
import { Column } from '../generated/entity/data/table';
|
||||
import {
|
||||
AssetCertification,
|
||||
Column,
|
||||
TagSource,
|
||||
} from '../generated/entity/data/table';
|
||||
import { Operation } from '../generated/entity/policies/policy';
|
||||
import { Paging } from '../generated/type/paging';
|
||||
import { LabelType, State, TagLabel } from '../generated/type/tagLabel';
|
||||
@ -306,12 +310,48 @@ export const createTierTag = (tag: Tag) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createCertificationTag = (tag: Tag) => {
|
||||
return {
|
||||
tagLabel: {
|
||||
displayName: tag.displayName,
|
||||
name: tag.name,
|
||||
href: tag.href,
|
||||
description: tag.description,
|
||||
tagFQN: tag.fullyQualifiedName,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
};
|
||||
};
|
||||
export const updateTierTag = (oldTags: Tag[] | TagLabel[], newTier?: Tag) => {
|
||||
return newTier
|
||||
? [...getTagsWithoutTier(oldTags), createTierTag(newTier)]
|
||||
: getTagsWithoutTier(oldTags);
|
||||
};
|
||||
|
||||
export const updateCertificationTag = (
|
||||
newCertification?: Tag
|
||||
): AssetCertification | undefined => {
|
||||
if (!newCertification) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
tagLabel: {
|
||||
tagFQN: newCertification.fullyQualifiedName || '',
|
||||
name: newCertification.name,
|
||||
displayName: newCertification.displayName,
|
||||
description: newCertification.description || '',
|
||||
source: TagSource.Classification,
|
||||
labelType: LabelType.Manual,
|
||||
state: State.Confirmed,
|
||||
style: newCertification.style,
|
||||
},
|
||||
appliedDate: Date.now(),
|
||||
expiryDate: Date.now() + 90 * 24 * 60 * 60 * 1000, // 90 days from now
|
||||
};
|
||||
};
|
||||
|
||||
export const createTagObject = (tags: EntityTags[]) => {
|
||||
return tags.map(
|
||||
(tag) =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user