From 4fa5a1d8bb74cd3f11c75a696726a0218910bb80 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Mon, 21 Aug 2023 20:07:34 +0530 Subject: [PATCH] chore(ui): added cypress tests for version pages and related bug fixes (#12943) * Fixed version page flaky no data placeholder removed code duplication in DataModelVersion component * fixed clear tier functionality for containers and data models * Added cypress tests for version pages * fixed unit tests --- .../ui/cypress/common/VersionUtils.js | 96 +++ .../resources/ui/cypress/common/common.js | 202 ++++-- .../ui/cypress/constants/Version.constants.js | 677 ++++++++++++++++++ .../e2e/Flow/AddAndRemoveTierAndOwner.spec.js | 84 +-- .../e2e/Pages/DataModelVersionPage.spec.js | 279 ++++++++ .../e2e/Pages/EntityVersionPages.spec.js | 289 ++++++++ .../ContainerVersion.component.tsx | 4 - .../ContainerVersion.test.tsx | 31 - .../DashboardVersion.component.tsx | 4 - .../DashboardVersion.test.tsx | 36 - .../DataModelVersion.component.tsx | 180 +---- .../DataModelVersion.interface.ts | 2 - .../components/DataModels/DataModelsTable.tsx | 8 +- .../ExploreSearchCard/ExploreSearchCard.tsx | 2 +- .../MlModelDetail/MlModelDetail.interface.ts | 1 - .../MlModelVersion.component.tsx | 6 +- .../MlModelVersion/MlModelVersion.test.tsx | 29 - .../PipelineVersion.component.tsx | 4 - .../PipelineVersion/PipelineVersion.test.tsx | 31 - .../TableVersion/TableVersion.component.tsx | 4 - .../TableVersion/TableVersion.test.tsx | 31 - .../TopicVersion/TopicVersion.component.tsx | 4 - .../TopicVersion/TopicVersion.test.tsx | 56 -- .../src/pages/ContainerPage/ContainerPage.tsx | 42 +- .../DataModelPage/DataModelPage.component.tsx | 40 +- .../EntityVersionPage.component.tsx | 441 ++++++------ .../EntityVersionPage.test.tsx | 104 +-- .../MlModelPage/MlModelPage.component.tsx | 42 +- .../main/resources/ui/src/rest/storageAPI.ts | 2 +- .../ui/src/utils/EntityVersionUtils.tsx | 3 +- 30 files changed, 1865 insertions(+), 869 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/cypress/common/VersionUtils.js create mode 100644 openmetadata-ui/src/main/resources/ui/cypress/constants/Version.constants.js create mode 100644 openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataModelVersionPage.spec.js create mode 100644 openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/EntityVersionPages.spec.js diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/VersionUtils.js b/openmetadata-ui/src/main/resources/ui/cypress/common/VersionUtils.js new file mode 100644 index 00000000000..2a430d56e62 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/VersionUtils.js @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// / + +import { isUndefined } from 'lodash'; +import { + interceptURL, + verifyResponseStatusCode, + visitDataModelPage, + visitEntityDetailsPage, +} from './common'; + +export const visitEntityDetailsVersionPage = ( + entityDetails, + id, + entityFQN, + version +) => { + visitEntityDetailsPage( + entityDetails.term, + entityDetails.serviceName, + entityDetails.entity, + undefined, + entityDetails.entityType + ); + + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/name/${entityFQN}?*include=all`, + 'getTableDetails' + ); + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/${id}/versions`, + 'getVersionsList' + ); + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/${id}/versions/${version ?? '*'}`, + 'getSelectedVersionDetails' + ); + + cy.get('[data-testid="version-button"]').as('versionButton'); + + if (!isUndefined(version)) { + cy.get('@versionButton').contains(version); + } + + cy.get('@versionButton').click(); + + verifyResponseStatusCode('@getTableDetails', 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); +}; + +export const visitDataModelVersionPage = ( + dataModelFQN, + dataModelId, + dataModelName, + version +) => { + visitDataModelPage(dataModelFQN, dataModelName); + + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/${dataModelId}/versions`, + 'getVersionsList' + ); + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/${dataModelId}/versions/${version ?? '*'}`, + 'getSelectedVersionDetails' + ); + + cy.get('[data-testid="version-button"]').as('versionButton'); + + if (!isUndefined(version)) { + cy.get('@versionButton').contains(version); + } + + cy.get('@versionButton').click(); + + verifyResponseStatusCode('@getDataModelDetails', 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); +}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/common.js b/openmetadata-ui/src/main/resources/ui/cypress/common/common.js index 61812f49e0c..5f6e7096b39 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/common.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/common.js @@ -1090,71 +1090,155 @@ export const updateDescriptionForIngestedTables = ( .should('contain', description); }; -export const followAndOwnTheEntity = (termObj) => { - // search for the term and redirect to the respective entity tab +export const addOwner = (ownerName, entity, isGlossaryPage) => { + cy.get('[data-testid="edit-owner"]').click(); + + interceptURL('GET', '/api/v1/users?&isBot=false&limit=15', 'getUsers'); + cy.get('.ant-tabs [id*=tab-users]').click(); + verifyResponseStatusCode('@getUsers', 200); - visitEntityDetailsPage(termObj.term, termObj.serviceName, termObj.entity); - // go to manage tab and search for logged in user and set the owner interceptURL( 'GET', - '/api/v1/search/query?q=*%20AND%20teamType:Group&from=0&size=15&index=team_search_index', - 'getTeams' + `api/v1/search/query?q=*${encodeURI(ownerName)}*`, + 'searchOwner' ); - cy.get('[data-testid="edit-owner"]').should('be.visible').click(); - verifyResponseStatusCode('@getTeams', 200); - // Clicking on users tab - cy.get('.user-team-select-popover') - .contains('Users') - .should('exist') - .should('be.visible') - .click(); + cy.get('[data-testid="owner-select-users-search-bar"]').type(ownerName); - cy.get('[data-testid="selectable-list"]') - .eq(1) - .should('exist') - .should('be.visible') - .find('[title="admin"]') - .should('be.visible') - .click(); + verifyResponseStatusCode('@searchOwner', 200); - cy.get('[data-testid="owner-link"]') - .scrollIntoView() - .invoke('text') - .then((text) => { - expect(text).equal('admin'); - }); + interceptURL('PATCH', `/api/v1/${entity}/*`, 'patchOwner'); - interceptURL('PUT', '/api/v1/*/*/followers', 'waitAfterFollow'); - cy.get('[data-testid="follow-button"]') - .scrollIntoView() - .should('be.visible') - .click(); - verifyResponseStatusCode('@waitAfterFollow', 200); - - cy.clickOnLogo(); - - cy.get('[data-testid="message-container"]') - .first() - .contains(`Followed ${termObj.entity.slice(0, -1)}`) - .should('be.visible'); - - // checks newly generated feed for follow and setting owner - cy.get('[data-testid="message-container"]') - .eq(1) - .scrollIntoView() - .contains('Added owner: admin') - .should('be.visible'); - - // Check followed entity on mydata page - cy.get('[data-testid="following-data-container"]') - .find(`[data-testid="following-${termObj.displayName}"]`) - .should('be.visible'); - - // Check owned entity - cy.get('[data-testid="my-data-container"]') - .find(`[data-testid="My data-${termObj.displayName}"]`) - .should('be.visible'); - - cy.clickOnLogo(); + cy.get(`.ant-popover [title="${ownerName}"]`).click(); + verifyResponseStatusCode('@patchOwner', 200); + if (isGlossaryPage) { + cy.get('[data-testid="glossary-owner-name"]').should('contain', ownerName); + } else { + cy.get('[data-testid="owner-link"]').should('contain', ownerName); + } +}; + +export const removeOwner = (entity, isGlossaryPage) => { + interceptURL('PATCH', `/api/v1/${entity}/*`, 'patchOwner'); + + cy.get('[data-testid="edit-owner"]').click(); + + cy.get('[data-testid="remove-owner"]').click(); + verifyResponseStatusCode('@patchOwner', 200); + if (isGlossaryPage) { + cy.get('[data-testid="glossary-owner-name"] > [data-testid="Add"]').should( + 'be.visible' + ); + } else { + cy.get('[data-testid="owner-link"]').should('contain', 'No Owner'); + } +}; + +export const addTier = (tier, entity) => { + interceptURL('PATCH', `/api/v1/${entity}/*`, 'patchTier'); + + interceptURL('GET', '/api/v1/tags?parent=Tier&limit=10', 'fetchTier'); + cy.get('[data-testid="edit-tier"]').click(); + verifyResponseStatusCode('@fetchTier', 200); + cy.get('[data-testid="radio-btn-Tier1"]').click({ waitForAnimations: true }); + verifyResponseStatusCode('@patchTier', 200); + cy.get('[data-testid="radio-btn-Tier1"]').should('be.checked'); + + cy.clickOutside(); + cy.get('[data-testid="Tier"]').should('contain', tier); +}; + +export const removeTier = (entity) => { + interceptURL('PATCH', `/api/v1/${entity}/*`, 'patchTier'); + + cy.get('[data-testid="edit-tier"]').click(); + cy.get('[data-testid="clear-tier"]').should('be.visible').click(); + + verifyResponseStatusCode('@patchTier', 200); + cy.get('[data-testid="Tier"]').should('contain', 'No Tier'); +}; + +export const deleteEntity = ( + entityName, + serviceName, + entity, + entityType, + successMessageEntityName, + deletionType = 'hard' +) => { + visitEntityDetailsPage( + entityName, + serviceName, + entity, + undefined, + entityType + ); + + cy.get('[data-testid="manage-button"]').click(); + + cy.get('[data-testid="delete-button-title"]').click(); + + cy.get('.ant-modal-header').should('contain', `Delete ${entityName}`); + + cy.get(`[data-testid="${deletionType}-delete-option"]`).click(); + + cy.get('[data-testid="confirm-button"]').should('be.disabled'); + cy.get('[data-testid="confirmation-text-input"]').type(DELETE_TERM); + + interceptURL( + 'DELETE', + `api/v1/${entity}/*?hardDelete=${deletionType === 'hard'}&recursive=false`, + `${deletionType}DeleteTable` + ); + cy.get('[data-testid="confirm-button"]').should('not.be.disabled'); + cy.get('[data-testid="confirm-button"]').click(); + verifyResponseStatusCode(`@${deletionType}DeleteTable`, 200); + + toastNotification(`${successMessageEntityName} deleted successfully!`, false); +}; + +export const visitDataModelPage = (dataModelFQN, dataModelName) => { + interceptURL('GET', '/api/v1/teams/name/*', 'getOrganization'); + + cy.get('[data-testid="appbar-item-settings"]').click(); + + verifyResponseStatusCode('@getOrganization', 200); + + interceptURL('GET', '/api/v1/services/dashboardServices*', 'getServices'); + + cy.get('[data-menu-id*="services.dashboards"]').scrollIntoView().click(); + + verifyResponseStatusCode('@getServices', 200); + + interceptURL( + 'GET', + 'api/v1/services/dashboardServices/name/sample_looker?fields=*', + 'getDashboardDetails' + ); + interceptURL( + 'GET', + '/api/v1/dashboard/datamodels?service=sample_looker&fields=*', + 'getDataModels' + ); + + cy.get('[data-testid="service-name-sample_looker"]').scrollIntoView().click(); + + verifyResponseStatusCode('@getDashboardDetails', 200); + verifyResponseStatusCode('@getDataModels', 200); + + cy.get('[data-testid="data-model"]').scrollIntoView().click(); + + verifyResponseStatusCode('@getDataModels', 200); + + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/name/${dataModelFQN}*`, + 'getDataModelDetails' + ); + + cy.get(`[data-testid="data-model-${dataModelName}"]`) + .scrollIntoView() + .click(); + + verifyResponseStatusCode('@getDataModelDetails', 200); }; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/Version.constants.js b/openmetadata-ui/src/main/resources/ui/cypress/constants/Version.constants.js new file mode 100644 index 00000000000..4fc80871c67 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/Version.constants.js @@ -0,0 +1,677 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const OWNER = 'Amber Green'; +export const TIER = 'Tier1'; + +const TABLE_DETAILS_FOR_VERSION_TEST = { + name: 'cypress_version_test_table', + columns: [ + { + name: 'user_id', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: + 'Unique identifier for the user of your Shopify POS or your Shopify admin.', + }, + { + name: 'shop_id', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: + 'The ID of the store. This column is a foreign key reference to the shop_id column in the dim.shop table.', + }, + { + name: 'name', + dataType: 'VARCHAR', + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'Name of the staff member.', + children: [ + { + name: 'first_name', + dataType: 'VARCHAR', + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'First name of the staff member.', + }, + { + name: 'last_name', + dataType: 'VARCHAR', + dataLength: 100, + dataTypeDisplay: 'varchar', + }, + ], + }, + { + name: 'email', + dataType: 'VARCHAR', + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'Email address of the staff member.', + }, + ], + databaseSchema: 'sample_data.ecommerce_db.shopify', +}; + +export const TABLE_PATCH_PAYLOAD = [ + { + op: 'add', + path: '/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.SpecialCategory', + }, + }, + { + op: 'add', + path: '/columns/2/children/1/description', + value: 'Last name of the staff member.', + }, + { + op: 'remove', + path: '/columns/2/children/0/description', + }, + { + op: 'add', + path: '/columns/0/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.Personal', + }, + }, + { + op: 'add', + path: '/columns/0/tags/1', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PII.Sensitive', + }, + }, + { + op: 'add', + path: '/description', + value: 'Description for cypress_version_test_table', + }, +]; + +const TOPIC_DETAILS_FOR_VERSION_TEST = { + name: 'cypress_version_test_topic', + service: 'sample_kafka', + messageSchema: { + schemaText: `{"type":"object","required":["name","age","club_name"],"properties":{"name":{"type":"object","required":["first_name","last_name"], + "properties":{"first_name":{"type":"string"},"last_name":{"type":"string"}}},"age":{"type":"integer"},"club_name":{"type":"string"}}}`, + schemaType: 'JSON', + schemaFields: [ + { + name: 'default', + dataType: 'RECORD', + fullyQualifiedName: 'sample_kafka.cypress_version_test_topic.default', + tags: [], + children: [ + { + name: 'name', + dataType: 'RECORD', + fullyQualifiedName: + 'sample_kafka.cypress_version_test_topic.default.name', + tags: [], + children: [ + { + name: 'first_name', + dataType: 'STRING', + description: 'Description for schema field first_name', + fullyQualifiedName: + 'sample_kafka.cypress_version_test_topic.default.name.first_name', + tags: [], + }, + { + name: 'last_name', + dataType: 'STRING', + fullyQualifiedName: + 'sample_kafka.cypress_version_test_topic.default.name.last_name', + tags: [], + }, + ], + }, + { + name: 'age', + dataType: 'INT', + fullyQualifiedName: + 'sample_kafka.cypress_version_test_topic.default.age', + tags: [], + }, + { + name: 'club_name', + dataType: 'STRING', + fullyQualifiedName: + 'sample_kafka.cypress_version_test_topic.default.club_name', + tags: [], + }, + ], + }, + ], + }, + partitions: 128, +}; + +export const TOPIC_PATCH_PAYLOAD = [ + { + op: 'add', + path: '/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.SpecialCategory', + }, + }, + { + op: 'add', + path: '/messageSchema/schemaFields/0/children/0/children/1/description', + value: 'Description for schema field last_name', + }, + { + op: 'remove', + path: '/messageSchema/schemaFields/0/children/0/children/0/description', + }, + { + op: 'add', + path: '/messageSchema/schemaFields/0/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.Personal', + }, + }, + { + op: 'add', + path: '/messageSchema/schemaFields/0/tags/1', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PII.Sensitive', + }, + }, + { + op: 'add', + path: '/description', + value: 'Description for cypress_version_test_topic', + }, +]; + +const DASHBOARD_DETAILS_FOR_VERSION_TEST = { + name: 'cypress_version_test_dashboard', + service: 'sample_superset', +}; + +const DASHBOARD_PATCH_PAYLOAD = [ + { + op: 'add', + path: '/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.SpecialCategory', + }, + }, + { + op: 'add', + path: '/description', + value: 'Description for cypress_version_test_dashboard', + }, +]; + +const PIPELINE_DETAILS_FOR_VERSION_TEST = { + name: 'cypress_version_test_pipeline', + tasks: [ + { + name: 'cypress_task_1', + displayName: 'cypress_task_1', + fullyQualifiedName: + 'sample_airflow.cypress_version_test_pipeline.cypress_task_1', + sourceUrl: + 'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=assert_table_exists', + downstreamTasks: [], + taskType: 'SnowflakeOperator', + tags: [], + }, + { + name: 'cypress_task_2', + displayName: 'cypress_task_2', + fullyQualifiedName: + 'sample_airflow.cypress_version_test_pipeline.cypress_task_2', + description: 'Description for task cypress_task_2', + sourceUrl: + 'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=assert_table_exists', + downstreamTasks: [], + taskType: 'HiveOperator', + tags: [], + }, + { + name: 'cypress_task_3', + displayName: 'cypress_task_3', + fullyQualifiedName: + 'sample_airflow.cypress_version_test_pipeline.cypress_task_3', + sourceUrl: + 'http://localhost:8080/taskinstance/list/?flt1_dag_id_equals=assert_table_exists', + downstreamTasks: [], + taskType: 'HiveOperator', + tags: [], + }, + ], + service: 'sample_airflow', +}; + +const PIPELINE_PATCH_PAYLOAD = [ + { + op: 'add', + path: '/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.SpecialCategory', + }, + }, + { + op: 'add', + path: '/tasks/2/description', + value: 'Description for task cypress_task_3', + }, + { + op: 'remove', + path: '/tasks/1/description', + }, + { + op: 'add', + path: '/tasks/0/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.Personal', + }, + }, + { + op: 'add', + path: '/tasks/0/tags/1', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PII.Sensitive', + }, + }, + { + op: 'add', + path: '/description', + value: 'Description for cypress_version_test_pipeline', + }, +]; + +const ML_MODEL_DETAILS_FOR_VERSION_TEST = { + name: 'cypress_version_test_ml_model', + algorithm: 'Neural Network', + mlFeatures: [ + { + name: 'feature_1', + dataType: 'numerical', + fullyQualifiedName: 'mlflow_svc.cypress_version_test_ml_model.feature_1', + featureSources: [], + tags: [], + }, + { + name: 'feature_2', + dataType: 'numerical', + description: 'Description for mlFeature feature_2', + fullyQualifiedName: 'mlflow_svc.cypress_version_test_ml_model.feature_2', + featureSources: [], + }, + { + name: 'feature_3', + dataType: 'numerical', + fullyQualifiedName: 'mlflow_svc.cypress_version_test_ml_model.feature_3', + featureSources: [], + }, + ], + tags: [], + service: 'mlflow_svc', +}; + +const ML_MODEL_PATCH_PAYLOAD = [ + { + op: 'add', + path: '/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.SpecialCategory', + }, + }, + { + op: 'add', + path: '/mlFeatures/2/description', + value: 'Description for mlFeature feature_3', + }, + { + op: 'remove', + path: '/mlFeatures/1/description', + }, + { + op: 'add', + path: '/mlFeatures/0/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.Personal', + }, + }, + { + op: 'add', + path: '/mlFeatures/0/tags/1', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PII.Sensitive', + }, + }, + { + op: 'add', + path: '/description', + value: 'Description for cypress_version_test_ml_model', + }, +]; + +const CONTAINER_DETAILS_FOR_VERSION_TEST = { + name: 'cypress_version_test_container', + service: 's3_storage_sample', + dataModel: { + isPartitioned: false, + columns: [ + { + name: 'column_1', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + fullyQualifiedName: + 's3_storage_sample.departments.finance.cypress_version_test_container.column_1', + tags: [], + ordinalPosition: 1, + }, + { + name: 'column_2', + dataType: 'BOOLEAN', + dataTypeDisplay: 'boolean', + description: 'Description for column column_2', + fullyQualifiedName: + 's3_storage_sample.departments.finance.cypress_version_test_container.column_2', + tags: [], + ordinalPosition: 2, + }, + { + name: 'column_3', + dataType: 'BOOLEAN', + dataTypeDisplay: 'boolean', + fullyQualifiedName: + 's3_storage_sample.departments.finance.cypress_version_test_container.column_3', + tags: [], + ordinalPosition: 3, + }, + { + name: 'column_4', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + fullyQualifiedName: + 's3_storage_sample.departments.finance.cypress_version_test_container.column_4', + tags: [], + ordinalPosition: 4, + }, + ], + }, + tags: [], +}; + +const CONTAINER_PATCH_PAYLOAD = [ + { + op: 'add', + path: '/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.SpecialCategory', + }, + }, + { + op: 'add', + path: '/dataModel/columns/2/description', + value: 'Description for column column_3', + }, + { + op: 'remove', + path: '/dataModel/columns/1/description', + }, + { + op: 'add', + path: '/dataModel/columns/0/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.Personal', + }, + }, + { + op: 'add', + path: '/dataModel/columns/0/tags/1', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PII.Sensitive', + }, + }, + { + op: 'add', + path: '/description', + value: 'Description for cypress_version_test_container', + }, +]; + +export const ENTITY_DETAILS_FOR_VERSION_TEST = { + Table: { + name: 'cypress_version_test_table', + term: 'cypress_version_test_table', + serviceName: 'sample_data', + entity: 'tables', + entityCreationDetails: TABLE_DETAILS_FOR_VERSION_TEST, + entityPatchPayload: TABLE_PATCH_PAYLOAD, + isChildrenExist: true, + childSelector: 'data-row-key', + entityAddedDescription: 'Description for cypress_version_test_table', + updatedTagEntityChildName: 'user_id', + entityChildRemovedDescription: 'First name of the staff member.', + entityChildAddedDescription: 'Last name of the staff member.', + }, + Topic: { + name: 'cypress_version_test_topic', + term: 'cypress_version_test_topic', + serviceName: 'sample_kafka', + entity: 'topics', + entityCreationDetails: TOPIC_DETAILS_FOR_VERSION_TEST, + entityPatchPayload: TOPIC_PATCH_PAYLOAD, + isChildrenExist: true, + childSelector: 'data-row-key', + entityAddedDescription: 'Description for cypress_version_test_topic', + updatedTagEntityChildName: 'default', + entityChildRemovedDescription: 'Description for schema field first_name', + entityChildAddedDescription: 'Description for schema field last_name', + }, + // TODO - Remove the comment after this issue is resolved https://github.com/open-metadata/OpenMetadata/issues/12924 + // Dashboard: { + // name: 'cypress_version_test_dashboard', + // term: 'cypress_version_test_dashboard', + // serviceName: 'sample_superset', + // entity: 'dashboards', + // entityCreationDetails: DASHBOARD_DETAILS_FOR_VERSION_TEST, + // entityPatchPayload: DASHBOARD_PATCH_PAYLOAD, + // isChildrenExist: false, + // entityAddedDescription: 'Description for cypress_version_test_dashboard', + // }, + Pipeline: { + name: 'cypress_version_test_pipeline', + term: 'cypress_version_test_pipeline', + serviceName: 'sample_airflow', + entity: 'pipelines', + entityCreationDetails: PIPELINE_DETAILS_FOR_VERSION_TEST, + entityPatchPayload: PIPELINE_PATCH_PAYLOAD, + isChildrenExist: true, + childSelector: 'data-row-key', + entityAddedDescription: 'Description for cypress_version_test_pipeline', + updatedTagEntityChildName: 'cypress_task_1', + entityChildRemovedDescription: 'Description for task cypress_task_2', + entityChildAddedDescription: 'Description for task cypress_task_3', + }, + 'ML Model': { + name: 'cypress_version_test_ml_model', + term: 'cypress_version_test_ml_model', + serviceName: 'mlflow_svc', + entity: 'mlmodels', + entityCreationDetails: ML_MODEL_DETAILS_FOR_VERSION_TEST, + entityPatchPayload: ML_MODEL_PATCH_PAYLOAD, + isChildrenExist: true, + childSelector: 'data-testid', + entityAddedDescription: 'Description for cypress_version_test_ml_model', + updatedTagEntityChildName: 'feature-card-feature_1', + entityChildRemovedDescription: 'Description for mlFeature feature_2', + entityChildAddedDescription: 'Description for mlFeature feature_3', + }, + Container: { + name: 'cypress_version_test_container', + term: 'cypress_version_test_container', + serviceName: 's3_storage_sample', + entity: 'containers', + entityCreationDetails: CONTAINER_DETAILS_FOR_VERSION_TEST, + entityPatchPayload: CONTAINER_PATCH_PAYLOAD, + isChildrenExist: true, + childSelector: 'data-row-key', + entityAddedDescription: 'Description for cypress_version_test_container', + updatedTagEntityChildName: 'column_1', + entityChildRemovedDescription: 'Description for column column_2', + entityChildAddedDescription: 'Description for column column_3', + }, +}; + +export const DATA_MODEL_DETAILS_FOR_VERSION_TEST = { + name: 'cypress_version_test_data_model', + service: 'sample_looker', + dataModelType: 'LookMlExplore', + columns: [ + { + name: 'column_1', + dataType: 'VARCHAR', + dataLength: 256, + dataTypeDisplay: 'varchar', + fullyQualifiedName: + 'sample_looker.model.cypress_version_test_data_model.column_1', + tags: [], + ordinalPosition: 1, + }, + { + name: 'column_2', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + description: 'Description for column column_2', + fullyQualifiedName: + 'sample_looker.model.cypress_version_test_data_model.column_2', + tags: [], + ordinalPosition: 2, + }, + { + name: 'column_3', + dataType: 'NUMERIC', + dataTypeDisplay: 'numeric', + fullyQualifiedName: + 'sample_looker.model.cypress_version_test_data_model.column_3', + tags: [], + ordinalPosition: 3, + }, + ], +}; + +export const DATA_MODEL_PATCH_PAYLOAD = [ + { + op: 'add', + path: '/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.SpecialCategory', + }, + }, + { + op: 'add', + path: '/columns/2/description', + value: 'Description for column column_3', + }, + { + op: 'remove', + path: '/columns/1/description', + }, + { + op: 'add', + path: '/columns/0/tags/0', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PersonalData.Personal', + }, + }, + { + op: 'add', + path: '/columns/0/tags/1', + value: { + labelType: 'Manual', + state: 'Confirmed', + source: 'Classification', + tagFQN: 'PII.Sensitive', + }, + }, + { + op: 'add', + path: '/description', + value: 'Description for cypress_version_test_data_model', + }, +]; + +export const DATA_MODEL_DETAILS = { + name: 'cypress_version_test_data_model', + entity: 'containers', + entityAddedDescription: 'Description for cypress_version_test_data_model', + updatedTagEntityChildName: 'column_1', + entityChildRemovedDescription: 'Description for column column_2', + entityChildAddedDescription: 'Description for column column_3', +}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddAndRemoveTierAndOwner.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddAndRemoveTierAndOwner.spec.js index c025b5bfde1..f5489a6a7a2 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddAndRemoveTierAndOwner.spec.js +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AddAndRemoveTierAndOwner.spec.js @@ -13,8 +13,12 @@ // / import { + addOwner, + addTier, descriptionBox, interceptURL, + removeOwner, + removeTier, verifyResponseStatusCode, visitEntityDetailsPage, } from '../../common/common'; @@ -44,58 +48,15 @@ const glossaryTerm = 'GlossaryTermOwnerTest'; const OWNER = 'Amber Green'; const TIER = 'Tier1'; -const addRemoveOwner = (isGlossaryPage) => { - cy.get('[data-testid="edit-owner"]').click(); - - cy.get('.ant-tabs [id*=tab-users]').click(); - verifyResponseStatusCode('@getUsers', 200); - - interceptURL( - 'GET', - `api/v1/search/query?q=*${encodeURI(OWNER)}*`, - 'searchOwner' - ); - - cy.get('[data-testid="owner-select-users-search-bar"]').type(OWNER); - - verifyResponseStatusCode('@searchOwner', 200); - - cy.get(`.ant-popover [title="${OWNER}"]`).click(); - verifyResponseStatusCode('@patchOwner', 200); - if (isGlossaryPage) { - cy.get('[data-testid="glossary-owner-name"]').should('contain', OWNER); - } else { - cy.get('[data-testid="owner-link"]').should('contain', OWNER); - } - cy.get('[data-testid="edit-owner"]').click(); - - cy.get('[data-testid="remove-owner"]').click(); - verifyResponseStatusCode('@patchOwner', 200); - if (isGlossaryPage) { - cy.get('[data-testid="glossary-owner-name"] > [data-testid="Add"]').should( - 'be.visible' - ); - } else { - cy.get('[data-testid="owner-link"]').should('contain', 'No Owner'); - } +const addRemoveOwner = (ownerName, entity, isGlossaryPage) => { + addOwner(ownerName, entity, isGlossaryPage); + removeOwner(entity, isGlossaryPage); }; -const addRemoveTier = () => { - interceptURL('GET', '/api/v1/tags?parent=Tier&limit=10', 'fetchTier'); - cy.get('[data-testid="edit-tier"]').click(); - verifyResponseStatusCode('@fetchTier', 200); - cy.get('[data-testid="radio-btn-Tier1"]').click({ waitForAnimations: true }); - verifyResponseStatusCode('@patchOwner', 200); - cy.get('[data-testid="radio-btn-Tier1"]').should('be.checked'); +const addRemoveTier = (tier, entity) => { + addTier(tier, entity); - cy.clickOutside(); - cy.get('[data-testid="Tier"]').should('contain', TIER); - - cy.get('[data-testid="edit-tier"]').click(); - cy.get('[data-testid="clear-tier"]').should('be.visible').click(); - - verifyResponseStatusCode('@patchOwner', 200); - cy.get('[data-testid="Tier"]').should('contain', 'No Tier'); + removeTier(entity); }; describe('Add and Remove Owner', () => { @@ -113,8 +74,6 @@ describe('Add and Remove Owner', () => { Object.entries(ENTITIES).map(([key, value]) => { it(`${key} details page`, () => { - interceptURL('PATCH', `/api/v1/${value.entity}/*`, 'patchOwner'); - visitEntityDetailsPage( value.term, value.serviceName, @@ -125,7 +84,7 @@ describe('Add and Remove Owner', () => { verifyResponseStatusCode('@entityPermission', 200); verifyResponseStatusCode('@activityFeed', 200); - addRemoveOwner(); + addRemoveOwner(OWNER, value.entity); }); }); @@ -144,7 +103,7 @@ describe('Add and Remove Owner', () => { verifyResponseStatusCode('@entityPermission', 200); verifyResponseStatusCode('@schemaDetails', 200); verifyResponseStatusCode('@activityFeed', 200); - addRemoveOwner(); + addRemoveOwner(OWNER, 'databaseSchemas'); }); it('database details page', () => { @@ -162,7 +121,7 @@ describe('Add and Remove Owner', () => { verifyResponseStatusCode('@entityPermission', 200); verifyResponseStatusCode('@databaseDetails', 200); verifyResponseStatusCode('@activityFeed', 200); - addRemoveOwner(); + addRemoveOwner(OWNER, 'databases'); }); it('service details page', () => { @@ -192,7 +151,7 @@ describe('Add and Remove Owner', () => { verifyResponseStatusCode('@serviceDetails', 200); verifyResponseStatusCode('@databases', 200); - addRemoveOwner(); + addRemoveOwner(OWNER, 'databaseServices'); }); it('Test suite details page', () => { @@ -219,7 +178,7 @@ describe('Add and Remove Owner', () => { verifyResponseStatusCode('@entityPermission', 200); verifyResponseStatusCode('@testSuiteDetails', 200); verifyResponseStatusCode('@testCases', 200); - addRemoveOwner(); + addRemoveOwner(OWNER, 'testSuites'); }); it('Teams details page', () => { @@ -235,7 +194,7 @@ describe('Add and Remove Owner', () => { verifyResponseStatusCode('@getOrganization', 200); verifyResponseStatusCode('@teamPermission', 200); - addRemoveOwner(); + addRemoveOwner(OWNER, 'teams'); }); it('Glossary details page', () => { @@ -259,7 +218,7 @@ describe('Add and Remove Owner', () => { verifyResponseStatusCode('@getGlossaries', 200); verifyResponseStatusCode('@glossaryPermission', 200); - addRemoveOwner(true); + addRemoveOwner(OWNER, 'glossaries', true); }); it('GlossaryTerm details page', () => { @@ -307,7 +266,7 @@ describe('Add and Remove Owner', () => { verifyResponseStatusCode('@glossaryTermPermission', 200); verifyResponseStatusCode('@getGlossaryTerms', 200); - addRemoveOwner(true); + addRemoveOwner(OWNER, 'glossaryTerms', true); }); it('Delete glossary and glossaryTerm', () => { @@ -365,8 +324,6 @@ describe('Add and Remove Tier', () => { Object.entries(ENTITIES).map(([key, value]) => { it(`${key} details page`, () => { - interceptURL('PATCH', `/api/v1/${value.entity}/*`, 'patchOwner'); - visitEntityDetailsPage( value.term, value.serviceName, @@ -377,12 +334,11 @@ describe('Add and Remove Tier', () => { verifyResponseStatusCode('@entityPermission', 200); verifyResponseStatusCode('@activityFeed', 200); - addRemoveTier(); + addRemoveTier(TIER, value.entity); }); }); it('database details page', () => { - interceptURL('PATCH', '/api/v1/databases/*', 'patchOwner'); interceptURL('GET', '/api/v1/databases/name/*', 'databaseDetails'); const value = ENTITIES.table; visitEntityDetailsPage(value.term, value.serviceName, value.entity); @@ -397,6 +353,6 @@ describe('Add and Remove Tier', () => { verifyResponseStatusCode('@databaseDetails', 200); verifyResponseStatusCode('@activityFeed', 200); - addRemoveTier(); + addRemoveTier(TIER, 'databases'); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataModelVersionPage.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataModelVersionPage.spec.js new file mode 100644 index 00000000000..df3f5b91605 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataModelVersionPage.spec.js @@ -0,0 +1,279 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// / + +import { + addOwner, + addTier, + interceptURL, + removeOwner, + removeTier, + toastNotification, + verifyResponseStatusCode, + visitDataModelPage, +} from '../../common/common'; +import { visitDataModelVersionPage } from '../../common/VersionUtils'; +import { DELETE_TERM } from '../../constants/constants'; +import { + DATA_MODEL_DETAILS, + DATA_MODEL_DETAILS_FOR_VERSION_TEST, + DATA_MODEL_PATCH_PAYLOAD, + OWNER, + TIER, +} from '../../constants/Version.constants'; + +describe('Data model version page should work properly', () => { + const dataModelName = DATA_MODEL_DETAILS.name; + let dataModelId; + let dataModelFQN; + + beforeEach(() => { + cy.login(); + }); + + it('Prerequisite for data model version page tests', () => { + const token = localStorage.getItem('oidcIdToken'); + + cy.request({ + method: 'PUT', + url: `/api/v1/dashboard/datamodels`, + headers: { Authorization: `Bearer ${token}` }, + body: DATA_MODEL_DETAILS_FOR_VERSION_TEST, + }).then((response) => { + expect(response.status).to.eq(201); + + dataModelId = response.body.id; + dataModelFQN = response.body.fullyQualifiedName; + + cy.request({ + method: 'PATCH', + url: `/api/v1/dashboard/datamodels/${dataModelId}`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json-patch+json', + }, + body: DATA_MODEL_PATCH_PAYLOAD, + }).then((response) => { + expect(response.status).to.eq(200); + }); + }); + }); + + it('Data model version page should show description and tag changes properly', () => { + visitDataModelVersionPage(dataModelFQN, dataModelId, dataModelName, '0.2'); + + cy.get( + `[data-testid="diff-added-${DATA_MODEL_DETAILS.entityAddedDescription}"]` + ) + .scrollIntoView() + .should('be.visible'); + + cy.get( + `[data-testid="entity-right-panel"] .diff-added [data-testid="tag-PersonalData.SpecialCategory"]` + ) + .scrollIntoView() + .should('be.visible'); + + cy.get( + `[data-row-key="${DATA_MODEL_DETAILS.updatedTagEntityChildName}"] .diff-added [data-testid="tag-PersonalData.Personal"]` + ) + .scrollIntoView() + .should('be.visible'); + + cy.get( + `[data-row-key="${DATA_MODEL_DETAILS.updatedTagEntityChildName}"] .diff-added [data-testid="tag-PII.Sensitive"]` + ) + .scrollIntoView() + .should('be.visible'); + + cy.get( + `[data-testid="diff-removed-${DATA_MODEL_DETAILS.entityChildRemovedDescription}"]` + ) + .scrollIntoView() + .should('be.visible'); + + cy.get( + `[data-testid="diff-added-${DATA_MODEL_DETAILS.entityChildAddedDescription}"]` + ) + .scrollIntoView() + .should('be.visible'); + }); + + it(`Data model version page should show removed tags changes properly`, () => { + visitDataModelPage(dataModelFQN, dataModelName); + + cy.get( + '[data-testid="entity-right-panel"] [data-testid="edit-button"]' + ).click(); + + cy.get( + '[data-testid="selected-tag-PersonalData.SpecialCategory"] [data-testid="remove-tags"]' + ).click(); + + interceptURL( + 'PATCH', + `/api/v1/dashboard/datamodels/${dataModelId}`, + `patchDataModel` + ); + + cy.get('[data-testid="saveAssociatedTag"]').click(); + + verifyResponseStatusCode(`@patchDataModel`, 200); + + cy.get('[data-testid="version-button"]').contains('0.3').click(); + + cy.get( + `[data-testid="entity-right-panel"] .diff-removed [data-testid="tag-PersonalData.SpecialCategory"]` + ) + .scrollIntoView() + .should('be.visible'); + }); + + it(`Data model version page should show owner changes properly`, () => { + visitDataModelPage(dataModelFQN, dataModelName); + + cy.get('[data-testid="version-button"]').as('versionButton'); + + cy.get('@versionButton').contains('0.3'); + + addOwner(OWNER, 'dashboard/datamodels'); + + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/name/${dataModelFQN}*`, + `getDataModelDetails` + ); + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/${dataModelId}/versions`, + 'getVersionsList' + ); + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/${dataModelId}/versions/0.4`, + 'getSelectedVersionDetails' + ); + + cy.get('@versionButton').contains('0.4').click(); + + verifyResponseStatusCode(`@getDataModelDetails`, 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); + + cy.get(`[data-testid="diff-added-${OWNER}"]`) + .scrollIntoView() + .should('be.visible'); + + cy.get('@versionButton').contains('0.4').click(); + + removeOwner('dashboard/datamodels'); + + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/${dataModelId}/versions/0.5`, + 'getSelectedVersionDetails' + ); + + cy.get('@versionButton').contains('0.5').click(); + + verifyResponseStatusCode(`@getDataModelDetails`, 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); + + cy.get(`[data-testid="diff-removed-${OWNER}"]`) + .scrollIntoView() + .should('be.visible'); + }); + + it(`Data model version page should show tier changes properly`, () => { + visitDataModelPage(dataModelFQN, dataModelName); + + cy.get('[data-testid="version-button"]').as('versionButton'); + + cy.get('@versionButton').contains('0.5'); + + addTier(TIER, 'dashboard/datamodels'); + + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/name/${dataModelFQN}*`, + `getDataModelDetails` + ); + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/${dataModelId}/versions`, + 'getVersionsList' + ); + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/${dataModelId}/versions/0.6`, + 'getSelectedVersionDetails' + ); + + cy.get('@versionButton').contains('0.6').click(); + + verifyResponseStatusCode(`@getDataModelDetails`, 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); + + cy.get(`[data-testid="diff-added-${TIER}"]`) + .scrollIntoView() + .should('be.visible'); + + cy.get('@versionButton').contains('0.6').click(); + + removeTier('dashboard/datamodels'); + + interceptURL( + 'GET', + `/api/v1/dashboard/datamodels/${dataModelId}/versions/0.7`, + 'getSelectedVersionDetails' + ); + + cy.get('@versionButton').contains('0.7').click(); + + verifyResponseStatusCode(`@getDataModelDetails`, 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); + + cy.get(`[data-testid="diff-removed-${TIER}"]`) + .scrollIntoView() + .should('be.visible'); + }); + + it(`Cleanup for data model version page test`, () => { + visitDataModelPage(dataModelFQN, dataModelName); + + cy.get('[data-testid="manage-button"]').click(); + + cy.get('[data-testid="delete-button-title"]').click(); + + cy.get('.ant-modal-header').should('contain', `Delete ${dataModelName}`); + + cy.get(`[data-testid="hard-delete-option"]`).click(); + + cy.get('[data-testid="confirm-button"]').should('be.disabled'); + cy.get('[data-testid="confirmation-text-input"]').type(DELETE_TERM); + + interceptURL( + 'DELETE', + `api/v1/dashboard/datamodels/*?hardDelete=true&recursive=false`, + `hardDeleteTable` + ); + cy.get('[data-testid="confirm-button"]').should('not.be.disabled'); + cy.get('[data-testid="confirm-button"]').click(); + verifyResponseStatusCode(`@hardDeleteTable`, 200); + + toastNotification(`Dashboard Data Model deleted successfully!`, false); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/EntityVersionPages.spec.js b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/EntityVersionPages.spec.js new file mode 100644 index 00000000000..21b5dca14a3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/EntityVersionPages.spec.js @@ -0,0 +1,289 @@ +/* + * Copyright 2023 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// / + +import { + addOwner, + addTier, + deleteEntity, + interceptURL, + removeOwner, + removeTier, + verifyResponseStatusCode, + visitEntityDetailsPage, +} from '../../common/common'; +import { visitEntityDetailsVersionPage } from '../../common/VersionUtils'; +import { + ENTITY_DETAILS_FOR_VERSION_TEST, + OWNER, + TIER, +} from '../../constants/Version.constants'; + +Object.entries(ENTITY_DETAILS_FOR_VERSION_TEST).map( + ([entityType, entityDetails]) => { + describe(`${entityType} version page should work properly`, () => { + const successMessageEntityName = + entityType === 'ML Model' ? 'Mlmodel' : entityType; + let entityId; + let entityFQN; + + beforeEach(() => { + cy.login(); + }); + + it(`Prerequisite for ${entityType} version page test`, () => { + const token = localStorage.getItem('oidcIdToken'); + + cy.request({ + method: 'PUT', + url: `/api/v1/${entityDetails.entity}`, + headers: { Authorization: `Bearer ${token}` }, + body: entityDetails.entityCreationDetails, + }).then((response) => { + expect(response.status).to.eq(201); + + entityId = response.body.id; + entityFQN = response.body.fullyQualifiedName; + + cy.request({ + method: 'PATCH', + url: `/api/v1/${entityDetails.entity}/${entityId}`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json-patch+json', + }, + body: entityDetails.entityPatchPayload, + }).then((response) => { + expect(response.status).to.eq(200); + }); + }); + }); + + it(`${entityType} version page should show description and tag changes properly`, () => { + visitEntityDetailsVersionPage( + entityDetails, + entityId, + entityFQN, + '0.2' + ); + cy.get( + `[data-testid="diff-added-${entityDetails.entityAddedDescription}"]` + ) + .scrollIntoView() + .should('be.visible'); + + cy.get( + `[data-testid="entity-right-panel"] .diff-added [data-testid="tag-PersonalData.SpecialCategory"]` + ) + .scrollIntoView() + .should('be.visible'); + + if (entityDetails.isChildrenExist) { + cy.get( + `[${entityDetails.childSelector}="${entityDetails.updatedTagEntityChildName}"] .diff-added [data-testid="tag-PersonalData.Personal"]` + ) + .scrollIntoView() + .should('be.visible'); + + cy.get( + `[${entityDetails.childSelector}="${entityDetails.updatedTagEntityChildName}"] .diff-added [data-testid="tag-PII.Sensitive"]` + ) + .scrollIntoView() + .should('be.visible'); + + cy.get( + `[data-testid="diff-removed-${entityDetails.entityChildRemovedDescription}"]` + ) + .scrollIntoView() + .should('be.visible'); + + cy.get( + `[data-testid="diff-added-${entityDetails.entityChildAddedDescription}"]` + ) + .scrollIntoView() + .should('be.visible'); + } + }); + + it(`${entityType} version page should show removed tags changes properly`, () => { + visitEntityDetailsPage( + entityDetails.term, + entityDetails.serviceName, + entityDetails.entity, + undefined, + entityType + ); + + cy.get( + '[data-testid="entity-right-panel"] [data-testid="edit-button"]' + ).click(); + + cy.get( + '[data-testid="selected-tag-PersonalData.SpecialCategory"] [data-testid="remove-tags"]' + ).click(); + + interceptURL( + 'PATCH', + `/api/v1/${entityDetails.entity}/${entityId}`, + `patch${entityType}` + ); + + cy.get('[data-testid="saveAssociatedTag"]').click(); + + verifyResponseStatusCode(`@patch${entityType}`, 200); + + cy.get('[data-testid="version-button"]').contains('0.3').click(); + + cy.get( + `[data-testid="entity-right-panel"] .diff-removed [data-testid="tag-PersonalData.SpecialCategory"]` + ) + .scrollIntoView() + .should('be.visible'); + }); + + it(`${entityType} version page should show owner changes properly`, () => { + visitEntityDetailsPage( + entityDetails.term, + entityDetails.serviceName, + entityDetails.entity, + undefined, + entityType + ); + + cy.get('[data-testid="version-button"]').as('versionButton'); + + cy.get('@versionButton').contains('0.3'); + + addOwner(OWNER, entityDetails.entity); + + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/name/${entityFQN}?*include=all`, + `get${entityType}Details` + ); + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/${entityId}/versions`, + 'getVersionsList' + ); + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/${entityId}/versions/0.4`, + 'getSelectedVersionDetails' + ); + + cy.get('@versionButton').contains('0.4').click(); + + verifyResponseStatusCode(`@get${entityType}Details`, 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); + + cy.get(`[data-testid="diff-added-${OWNER}"]`) + .scrollIntoView() + .should('be.visible'); + + cy.get('@versionButton').contains('0.4').click(); + + removeOwner(entityDetails.entity); + + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/${entityId}/versions/0.5`, + 'getSelectedVersionDetails' + ); + + cy.get('@versionButton').contains('0.5').click(); + + verifyResponseStatusCode(`@get${entityType}Details`, 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); + + cy.get(`[data-testid="diff-removed-${OWNER}"]`) + .scrollIntoView() + .should('be.visible'); + }); + + it(`${entityType} version page should show tier changes properly`, () => { + visitEntityDetailsPage( + entityDetails.term, + entityDetails.serviceName, + entityDetails.entity, + undefined, + entityType + ); + + cy.get('[data-testid="version-button"]').as('versionButton'); + + cy.get('@versionButton').contains('0.5'); + + addTier(TIER, entityDetails.entity); + + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/name/${entityFQN}?*include=all`, + `get${entityType}Details` + ); + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/${entityId}/versions`, + 'getVersionsList' + ); + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/${entityId}/versions/0.6`, + 'getSelectedVersionDetails' + ); + + cy.get('@versionButton').contains('0.6').click(); + + verifyResponseStatusCode(`@get${entityType}Details`, 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); + + cy.get(`[data-testid="diff-added-${TIER}"]`) + .scrollIntoView() + .should('be.visible'); + + cy.get('@versionButton').contains('0.6').click(); + + removeTier(entityDetails.entity); + + interceptURL( + 'GET', + `/api/v1/${entityDetails.entity}/${entityId}/versions/0.7`, + 'getSelectedVersionDetails' + ); + + cy.get('@versionButton').contains('0.7').click(); + + verifyResponseStatusCode(`@get${entityType}Details`, 200); + verifyResponseStatusCode('@getVersionsList', 200); + verifyResponseStatusCode('@getSelectedVersionDetails', 200); + + cy.get(`[data-testid="diff-removed-${TIER}"]`) + .scrollIntoView() + .should('be.visible'); + }); + + it(`Cleanup for ${entityType} version page test`, () => { + deleteEntity( + entityDetails.name, + entityDetails.serviceName, + entityDetails.entity, + entityType, + successMessageEntityName + ); + }); + }); + } +); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContainerVersion/ContainerVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContainerVersion/ContainerVersion.component.tsx index 4b6dfbe135c..1137fe0e146 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContainerVersion/ContainerVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContainerVersion/ContainerVersion.component.tsx @@ -215,10 +215,6 @@ const ContainerVersion: React.FC = ({ ] ); - if (!(entityPermissions.ViewAll || entityPermissions.ViewBasic)) { - return ; - } - return ( <> {isVersionLoading ? ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ContainerVersion/ContainerVersion.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ContainerVersion/ContainerVersion.test.tsx index a711eb2e30a..cd2be040439 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ContainerVersion/ContainerVersion.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ContainerVersion/ContainerVersion.test.tsx @@ -117,37 +117,6 @@ describe('ContainerVersion tests', () => { expect(versionTable).toBeNull(); }); - it('Should display ErrorPlaceholder if no viewing permission', async () => { - await act(async () => { - render( - - ); - }); - - const errorPlaceHolder = screen.getByText('ErrorPlaceHolder'); - const loader = screen.queryByText('Loader'); - const dataAssetsVersionHeader = screen.queryByText( - 'DataAssetsVersionHeader' - ); - const schemaTabLabel = screen.queryByText('label.schema'); - const customPropertyTabLabel = screen.queryByText( - 'label.custom-property-plural' - ); - const entityVersionTimeLine = screen.queryByText('EntityVersionTimeLine'); - const versionTable = screen.queryByText('VersionTable'); - - expect(errorPlaceHolder).toBeInTheDocument(); - expect(loader).toBeNull(); - expect(entityVersionTimeLine).toBeNull(); - expect(dataAssetsVersionHeader).toBeNull(); - expect(schemaTabLabel).toBeNull(); - expect(customPropertyTabLabel).toBeNull(); - expect(versionTable).toBeNull(); - }); - it('Should update url on click of tab', async () => { await act(async () => { render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardVersion/DashboardVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DashboardVersion/DashboardVersion.component.tsx index 29b7dcd494b..a885b79a439 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardVersion/DashboardVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardVersion/DashboardVersion.component.tsx @@ -234,10 +234,6 @@ const DashboardVersion: FC = ({ [description, tableColumn, currentVersionData, entityPermissions] ); - if (!(entityPermissions.ViewAll || entityPermissions.ViewBasic)) { - return ; - } - return ( <> {isVersionLoading ? ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardVersion/DashboardVersion.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DashboardVersion/DashboardVersion.test.tsx index f162158e965..9dfc74b5873 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardVersion/DashboardVersion.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardVersion/DashboardVersion.test.tsx @@ -16,7 +16,6 @@ import userEvent from '@testing-library/user-event'; import { ENTITY_PERMISSIONS } from 'mocks/Permissions.mock'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; -import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils'; import { dashboardVersionProps, mockNoChartData, @@ -222,39 +221,4 @@ describe('DashboardVersion tests', () => { '/dashboard/sample_superset.eta_predictions_performance/versions/0.3/custom_properties' ); }); - - it('Should display ErrorPlaceholder if no viewing permission', async () => { - await act(async () => { - render( - , - { - wrapper: MemoryRouter, - } - ); - }); - - const versionData = screen.queryByTestId('version-data'); - const schemaTable = screen.queryByTestId('schema-table'); - - const tabs = screen.queryByTestId('tabs'); - const description = screen.queryByText('Description.component'); - const richTextEditorPreviewer = screen.queryByText( - 'RichTextEditorPreviewer.component' - ); - const entityVersionTimeLine = screen.queryByText( - 'EntityVersionTimeLine.component' - ); - const errorPlaceHolder = screen.getByText('ErrorPlaceHolder'); - - expect(entityVersionTimeLine).toBeNull(); - expect(versionData).toBeNull(); - expect(schemaTable).toBeNull(); - expect(tabs).toBeNull(); - expect(description).toBeNull(); - expect(richTextEditorPreviewer).toBeNull(); - expect(errorPlaceHolder).toBeInTheDocument(); - }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataModelVersion/DataModelVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataModelVersion/DataModelVersion.component.tsx index c53a91d0bf2..290c7ad3b1f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataModelVersion/DataModelVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataModelVersion/DataModelVersion.component.tsx @@ -14,13 +14,11 @@ import { Col, Row, Space, Tabs, TabsProps } from 'antd'; import classNames from 'classnames'; import DescriptionV1 from 'components/common/description/DescriptionV1'; -import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder'; import DataAssetsVersionHeader from 'components/DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; import EntityVersionTimeLine from 'components/Entity/EntityVersionTimeLine/EntityVersionTimeLine'; import TabsLabel from 'components/TabsLabel/TabsLabel.component'; import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2'; import VersionTable from 'components/VersionTable/VersionTable.component'; -import { ERROR_PLACEHOLDER_TYPE } from 'enums/common.enum'; import { EntityTabs, EntityType, FqnPart } from 'enums/entity.enum'; import { ChangeDescription, @@ -28,27 +26,18 @@ import { DashboardDataModel, } from 'generated/entity/data/dashboardDataModel'; import { TagSource } from 'generated/type/schema'; -import { EntityDiffProps } from 'interface/EntityVersion.interface'; -import { cloneDeep, isEqual } from 'lodash'; +import { cloneDeep } from 'lodash'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { getPartialNameFromTableFQN } from 'utils/CommonUtils'; import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants'; import { EntityField } from '../../constants/Feeds.constants'; -import { TagLabel } from '../../generated/type/tagLabel'; import { - getChangedEntityName, - getChangedEntityNewValue, - getChangedEntityOldValue, + getColumnsDataWithVersionChanges, getCommonExtraInfoForVersionDetails, - getDiffByFieldName, getEntityVersionByField, getEntityVersionTags, - getTagsDiff, - getTextDiff, - isEndsWithField, } from '../../utils/EntityVersionUtils'; -import { TagLabelWithStatus } from '../../utils/EntityVersionUtils.interface'; import Loader from '../Loader/Loader'; import { DataModelVersionProp } from './DataModelVersion.interface'; @@ -64,181 +53,24 @@ const DataModelVersion: FC = ({ backHandler, versionHandler, dataModelFQN, - entityPermissions, }: DataModelVersionProp) => { const { t } = useTranslation(); const [changeDescription, setChangeDescription] = useState( currentVersionData.changeDescription as ChangeDescription ); - const getChangeColName = (name: string | undefined) => { - const nameArr = name?.split(FQN_SEPARATOR_CHAR); - - if (nameArr?.length === 3) { - return nameArr.slice(-2, -1)[0]; - } else { - return nameArr?.slice(-3, -1)?.join('.'); - } - }; - const { ownerDisplayName, ownerRef, tierDisplayName } = useMemo( () => getCommonExtraInfoForVersionDetails(changeDescription, owner, tier), [changeDescription, owner, tier] ); - const handleColumnDescriptionChangeDiff = ( - colList: DashboardDataModel['columns'], - columnsDiff: EntityDiffProps, - changedColName: string | undefined - ) => { - const oldDescription = getChangedEntityOldValue(columnsDiff); - const newDescription = getChangedEntityNewValue(columnsDiff); - - const formatColumnData = (arr: DashboardDataModel['columns']) => { - arr?.forEach((i) => { - if (isEqual(i.name, changedColName)) { - i.description = getTextDiff( - oldDescription ?? '', - newDescription ?? '', - i.description - ); - } else { - formatColumnData(i?.children as DashboardDataModel['columns']); - } - }); - }; - - formatColumnData(colList); - }; - - const handleColumnTagChangeDiff = ( - colList: DashboardDataModel['columns'], - columnsDiff: EntityDiffProps, - changedColName: string | undefined - ) => { - const oldTags: Array = JSON.parse( - getChangedEntityOldValue(columnsDiff) ?? '[]' - ); - const newTags: Array = JSON.parse( - getChangedEntityNewValue(columnsDiff) ?? '[]' - ); - - const formatColumnData = (arr: DashboardDataModel['columns']) => { - arr?.forEach((i) => { - if (isEqual(i.name, changedColName)) { - const flag: { [x: string]: boolean } = {}; - const uniqueTags: Array = []; - const tagsDiff = getTagsDiff(oldTags, newTags); - [...tagsDiff, ...(i.tags as Array)].forEach( - (elem: TagLabelWithStatus) => { - if (!flag[elem.tagFQN]) { - flag[elem.tagFQN] = true; - uniqueTags.push(elem); - } - } - ); - i.tags = uniqueTags; - } else { - formatColumnData(i?.children as DashboardDataModel['columns']); - } - }); - }; - - formatColumnData(colList); - }; - - const handleColumnDiffAdded = ( - colList: DashboardDataModel['columns'], - columnsDiff: EntityDiffProps - ) => { - const newCol: Array = JSON.parse( - columnsDiff.added?.newValue ?? '[]' - ); - newCol.forEach((col) => { - const formatColumnData = (arr: DashboardDataModel['columns']) => { - arr?.forEach((i) => { - if (isEqual(i.name, col.name)) { - i.tags = col.tags?.map((tag) => ({ ...tag, added: true })); - i.description = getTextDiff('', col.description ?? ''); - i.dataTypeDisplay = getTextDiff('', col.dataTypeDisplay ?? ''); - i.name = getTextDiff('', col.name); - } else { - formatColumnData(i?.children as DashboardDataModel['columns']); - } - }); - }; - formatColumnData(colList); - }); - }; - - const handleColumnDiffDeleted = (columnsDiff: EntityDiffProps) => { - const newCol: Array = JSON.parse( - columnsDiff.deleted?.oldValue ?? '[]' - ); - - return newCol.map((col) => ({ - ...col, - tags: col.tags?.map((tag) => ({ ...tag, removed: true })), - description: getTextDiff(col.description ?? '', ''), - dataTypeDisplay: getTextDiff(col.dataTypeDisplay ?? '', ''), - name: getTextDiff(col.name, ''), - })); - }; - const columns: DashboardDataModel['columns'] = useMemo(() => { const colList = cloneDeep( (currentVersionData as DashboardDataModel).columns || [] ); - const columnsDiff = getDiffByFieldName( - EntityField.COLUMNS, - changeDescription - ); - const changedColName = getChangeColName(getChangedEntityName(columnsDiff)); - const colNameWithoutQuotes = changedColName?.replaceAll(/(^")|("$)/g, ''); - if ( - isEndsWithField( - EntityField.DESCRIPTION, - getChangedEntityName(columnsDiff) - ) - ) { - handleColumnDescriptionChangeDiff( - colList, - columnsDiff, - colNameWithoutQuotes - ); - - return colList; - } else if ( - isEndsWithField(EntityField.TAGS, getChangedEntityName(columnsDiff)) - ) { - handleColumnTagChangeDiff(colList, columnsDiff, colNameWithoutQuotes); - - return colList; - } else { - const columnsDiff = getDiffByFieldName( - EntityField.COLUMNS, - changeDescription, - true - ); - let newColumns: Column[] = []; - if (columnsDiff.added) { - handleColumnDiffAdded(colList, columnsDiff); - } - if (columnsDiff.deleted) { - newColumns = handleColumnDiffDeleted(columnsDiff); - } else { - return colList; - } - - return [...newColumns, ...colList]; - } - }, [ - currentVersionData, - changeDescription, - getChangeColName, - handleColumnDescriptionChangeDiff, - ]); + return getColumnsDataWithVersionChanges(changeDescription, colList); + }, [currentVersionData, changeDescription]); useEffect(() => { setChangeDescription( @@ -319,10 +151,6 @@ const DataModelVersion: FC = ({ [description, dataModelFQN, columns] ); - if (!(entityPermissions.ViewAll || entityPermissions.ViewBasic)) { - return ; - } - return ( <>
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataModelVersion/DataModelVersion.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DataModelVersion/DataModelVersion.interface.ts index b5d63b94517..71f2e61215a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataModelVersion/DataModelVersion.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataModelVersion/DataModelVersion.interface.ts @@ -11,7 +11,6 @@ * limitations under the License. */ -import { OperationPermission } from 'components/PermissionProvider/PermissionProvider.interface'; import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel'; import { VersionData } from 'pages/EntityVersionPage/EntityVersionPage.component'; import { EntityHistory } from '../../generated/type/entityHistory'; @@ -31,5 +30,4 @@ export interface DataModelVersionProp { deleted?: boolean; backHandler: () => void; versionHandler: (v: string) => void; - entityPermissions: OperationPermission; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataModels/DataModelsTable.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataModels/DataModelsTable.tsx index acaf9b1f687..aad6aa27cb9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataModels/DataModelsTable.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataModels/DataModelsTable.tsx @@ -43,9 +43,13 @@ const DataModelTable = ({ key: 'displayName', width: 350, render: (_, record: ServicePageData) => { + const dataModelDisplayName = getEntityName(record); + return ( - - {getEntityName(record)} + + {dataModelDisplayName} ); }, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx index 062c8d07ef5..c4fdee60648 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreSearchCard/ExploreSearchCard.tsx @@ -123,7 +123,7 @@ const ExploreSearchCard: React.FC = forwardRef<
)} - + {isTourOpen ? (