From 1081fb2227baf2e70a4b915f8d2aa192280fcf4e Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Tue, 6 Aug 2024 18:15:15 +0530 Subject: [PATCH 01/10] Add playwright test for a user with DataConsumer role --- .../ui/playwright/e2e/Pages/Entity.spec.ts | 2 +- .../e2e/Pages/EntityDataConsumer.spec.ts | 176 ++++++++++++ .../e2e/Pages/ServiceEntity.spec.ts | 2 +- .../support/entity/ApiEndpointClass.ts | 74 ++--- .../support/entity/DashboardDataModelClass.ts | 23 +- .../playwright/support/entity/EntityClass.ts | 174 ++++++++---- .../playwright/support/entity/MlModelClass.ts | 19 +- .../support/entity/PipelineClass.ts | 7 +- .../support/entity/SearchIndexClass.ts | 54 +++- .../playwright/support/entity/TableClass.ts | 94 +++--- .../playwright/support/entity/TopicClass.ts | 77 ++--- .../resources/ui/playwright/utils/domain.ts | 14 +- .../resources/ui/playwright/utils/entity.ts | 267 ++++++++++++++++-- .../DataModels/DataModelDetails.component.tsx | 2 +- .../AlertDetailsPage/AlertDetailsPage.tsx | 3 + .../NotificationListPage.tsx | 1 + 16 files changed, 759 insertions(+), 230 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts index 823bf3867fb5..37572d751eb6 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts @@ -148,7 +148,7 @@ entities.forEach((EntityClass) => { await test.step(`Set ${titleText} Custom Property`, async () => { for (const type of properties) { - await entity.setCustomProperty( + await entity.updateCustomProperty( page, entity.customPropertyValue[type].property, entity.customPropertyValue[type].value diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts new file mode 100644 index 000000000000..70bee2d41a5d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts @@ -0,0 +1,176 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, Page, test as base } from '@playwright/test'; +import { isUndefined } from 'lodash'; +import { ApiEndpointClass } from '../../support/entity/ApiEndpointClass'; +import { ContainerClass } from '../../support/entity/ContainerClass'; +import { DashboardClass } from '../../support/entity/DashboardClass'; +import { DashboardDataModelClass } from '../../support/entity/DashboardDataModelClass'; +import { EntityDataClass } from '../../support/entity/EntityDataClass'; +import { MlModelClass } from '../../support/entity/MlModelClass'; +import { PipelineClass } from '../../support/entity/PipelineClass'; +import { SearchIndexClass } from '../../support/entity/SearchIndexClass'; +import { StoredProcedureClass } from '../../support/entity/StoredProcedureClass'; +import { TableClass } from '../../support/entity/TableClass'; +import { TopicClass } from '../../support/entity/TopicClass'; +import { UserClass } from '../../support/user/UserClass'; +import { performAdminLogin } from '../../utils/admin'; +import { redirectToHomePage } from '../../utils/common'; + +const user = new UserClass(); + +const entities = [ + ApiEndpointClass, + TableClass, + StoredProcedureClass, + DashboardClass, + PipelineClass, + TopicClass, + MlModelClass, + ContainerClass, + SearchIndexClass, + DashboardDataModelClass, +] as const; + +// Create 2 page and authenticate 1 with admin and another with normal user +const test = base.extend<{ + userPage: Page; +}>({ + userPage: async ({ browser }, use) => { + const page = await browser.newPage(); + await user.login(page); + await use(page); + await page.close(); + }, +}); + +entities.forEach((EntityClass) => { + const entity = new EntityClass(); + + test.describe(entity.getType(), () => { + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + + await user.create(apiContext); + await EntityDataClass.preRequisitesForTests(apiContext); + await entity.create(apiContext); + await afterAction(); + }); + + test.beforeEach('Visit entity details page', async ({ userPage }) => { + await redirectToHomePage(userPage); + await entity.visitEntityPage(userPage); + }); + + test('User as Owner Add, Update and Remove', async ({ userPage }) => { + test.slow(true); + + const OWNER1 = EntityDataClass.user1.getUserName(); + const OWNER2 = EntityDataClass.user2.getUserName(); + const OWNER3 = EntityDataClass.user3.getUserName(); + await entity.owner( + userPage, + [OWNER1, OWNER3], + [OWNER2], + undefined, + false + ); + }); + + test('No edit owner permission', async ({ userPage }) => { + await userPage.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + await expect(userPage.getByTestId('edit-owner')).not.toBeAttached(); + }); + + test('Tier Add, Update and Remove', async ({ userPage }) => { + await entity.tier( + userPage, + 'Tier1', + EntityDataClass.tierTag1.data.displayName + ); + }); + + test('Update description', async ({ userPage }) => { + await entity.descriptionUpdate(userPage); + }); + + test('Tag Add, Update and Remove', async ({ userPage }) => { + await entity.tag(userPage, 'PersonalData.Personal', 'PII.None'); + }); + + test('Glossary Term Add, Update and Remove', async ({ userPage }) => { + await entity.glossaryTerm( + userPage, + EntityDataClass.glossaryTerm1.responseData, + EntityDataClass.glossaryTerm2.responseData + ); + }); + + // Run only if entity has children + if (!isUndefined(entity.childrenTabId)) { + test('Tag Add, Update and Remove for child entities', async ({ + userPage, + }) => { + await userPage.getByTestId(entity.childrenTabId ?? '').click(); + + await entity.tagChildren({ + page: userPage, + tag1: 'PersonalData.Personal', + tag2: 'PII.None', + rowId: entity.childrenSelectorId ?? '', + rowSelector: + entity.type === 'MlModel' ? 'data-testid' : 'data-row-key', + }); + }); + } + + // Run only if entity has children + if (!isUndefined(entity.childrenTabId)) { + test('Glossary Term Add, Update and Remove for child entities', async ({ + userPage, + }) => { + await userPage.getByTestId(entity.childrenTabId ?? '').click(); + + await entity.glossaryTermChildren({ + page: userPage, + glossaryTerm1: EntityDataClass.glossaryTerm1.responseData, + glossaryTerm2: EntityDataClass.glossaryTerm2.responseData, + rowId: entity.childrenSelectorId ?? '', + rowSelector: + entity.type === 'MlModel' ? 'data-testid' : 'data-row-key', + }); + }); + } + + test(`UpVote & DownVote entity`, async ({ userPage }) => { + await entity.upVote(userPage); + await entity.downVote(userPage); + }); + + test(`Follow & Un-follow entity`, async ({ userPage }) => { + const entityName = entity.entityResponseData?.['displayName']; + await entity.followUnfollowEntity(userPage, entityName); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await user.delete(apiContext); + await entity.delete(apiContext); + await EntityDataClass.postRequisitesForTests(apiContext); + await afterAction(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts index b272050c2462..00aaade71ce5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ServiceEntity.spec.ts @@ -136,7 +136,7 @@ entities.forEach((EntityClass) => { await test.step(`Set ${titleText} Custom Property`, async () => { for (const type of properties) { - await entity.setCustomProperty( + await entity.updateCustomProperty( page, entity.customPropertyValue[type].property, entity.customPropertyValue[type].value diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiEndpointClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiEndpointClass.ts index 15e4de607042..0c7b6b0c49ce 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiEndpointClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/ApiEndpointClass.ts @@ -38,56 +38,58 @@ export class ApiEndpointClass extends EntityClass { private apiEndpointName = `pw-api-endpoint-${uuid()}`; private fqn = `${this.service.name}.${this.apiCollection.name}.${this.apiEndpointName}`; - entity = { - name: this.apiEndpointName, - apiCollection: `${this.service.name}.${this.apiCollection.name}`, - endpointURL: 'https://sandbox-beta.open-metadata.org/swagger.json', - requestSchema: { - schemaType: 'JSON', - schemaFields: [ + children = [ + { + name: 'default', + dataType: 'RECORD', + fullyQualifiedName: `${this.fqn}.default`, + tags: [], + children: [ { - name: 'default', + name: 'name', dataType: 'RECORD', - fullyQualifiedName: `${this.fqn}.default`, + fullyQualifiedName: `${this.fqn}.default.name`, tags: [], children: [ { - name: 'name', - dataType: 'RECORD', - fullyQualifiedName: `${this.fqn}.default.name`, - tags: [], - children: [ - { - name: 'first_name', - dataType: 'STRING', - description: 'Description for schema field first_name', - fullyQualifiedName: `${this.fqn}.default.name.first_name`, - tags: [], - }, - { - name: 'last_name', - dataType: 'STRING', - fullyQualifiedName: `${this.fqn}.default.name.last_name`, - tags: [], - }, - ], - }, - { - name: 'age', - dataType: 'INT', - fullyQualifiedName: `${this.fqn}.default.age`, + name: 'first_name', + dataType: 'STRING', + description: 'Description for schema field first_name', + fullyQualifiedName: `${this.fqn}.default.name.first_name`, tags: [], }, { - name: 'club_name', + name: 'last_name', dataType: 'STRING', - fullyQualifiedName: `${this.fqn}.default.club_name`, + fullyQualifiedName: `${this.fqn}.default.name.last_name`, tags: [], }, ], }, + { + name: 'age', + dataType: 'INT', + fullyQualifiedName: `${this.fqn}.default.age`, + tags: [], + }, + { + name: 'club_name', + dataType: 'STRING', + fullyQualifiedName: `${this.fqn}.default.club_name`, + tags: [], + }, ], }, + ]; + + entity = { + name: this.apiEndpointName, + apiCollection: `${this.service.name}.${this.apiCollection.name}`, + endpointURL: 'https://sandbox-beta.open-metadata.org/swagger.json', + requestSchema: { + schemaType: 'JSON', + schemaFields: this.children, + }, responseSchema: { schemaType: 'JSON', schemaFields: [ @@ -143,6 +145,8 @@ export class ApiEndpointClass extends EntityClass { super(EntityTypeEndpoint.API_ENDPOINT); this.service.name = name ?? this.service.name; this.type = 'ApiEndpoint'; + this.childrenTabId = 'schema'; + this.childrenSelectorId = this.children[0].name; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts index bb0a4d156b85..0a498ce47a89 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts @@ -33,19 +33,22 @@ export class DashboardDataModelClass extends EntityClass { }, }, }; + + children = [ + { + name: 'country_name', + dataType: 'VARCHAR', + dataLength: 256, + dataTypeDisplay: 'varchar', + description: 'Name of the country.', + }, + ]; + entity = { name: `pw-dashboard-data-model-${uuid()}`, displayName: `pw-dashboard-data-model-${uuid()}`, service: this.service.name, - columns: [ - { - name: 'country_name', - dataType: 'VARCHAR', - dataLength: 256, - dataTypeDisplay: 'varchar', - description: 'Name of the country.', - }, - ], + columns: this.children, dataModelType: 'SupersetDataModel', }; @@ -56,6 +59,8 @@ export class DashboardDataModelClass extends EntityClass { super(EntityTypeEndpoint.DataModel); this.service.name = name ?? this.service.name; this.type = 'Dashboard Data Model'; + this.childrenTabId = 'model'; + this.childrenSelectorId = this.children[0].name; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityClass.ts index ff2979b9f9c7..6bd0b8d67361 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityClass.ts @@ -23,7 +23,9 @@ import { addMultiOwner, addOwner, assignGlossaryTerm, + assignGlossaryTermToChildren, assignTag, + assignTagToChildren, assignTier, createAnnouncement, createInactiveAnnouncement, @@ -32,8 +34,10 @@ import { followEntity, hardDeleteEntity, removeGlossaryTerm, + removeGlossaryTermFromChildren, removeOwner, removeTag, + removeTagsFromChildren, removeTier, replyAnnouncement, softDeleteEntity, @@ -50,6 +54,8 @@ import { EntityTypeEndpoint, ENTITY_PATH } from './Entity.interface'; export class EntityClass { type: string; + childrenTabId?: string; + childrenSelectorId?: string; endpoint: EntityTypeEndpoint; cleanupUser: (apiContext: APIRequestContext) => Promise; @@ -120,52 +126,57 @@ export class EntityClass { page: Page, owner1: string[], owner2: string[], - type: 'Teams' | 'Users' = 'Users' + type: 'Teams' | 'Users' = 'Users', + isEditPermission = true ) { if (type === 'Teams') { - await addOwner( + await addOwner({ page, - owner1[0], + owner: owner1[0], type, - this.endpoint, - 'data-assets-header' - ); - await updateOwner( - page, - owner2[0], - type, - this.endpoint, - 'data-assets-header' - ); - await removeOwner( - page, - this.endpoint, - owner2[0], - type, - 'data-assets-header' - ); - } else { - await addMultiOwner({ - page, - ownerNames: owner1, - activatorBtnDataTestId: 'edit-owner', - resultTestId: 'data-assets-header', endpoint: this.endpoint, + dataTestId: 'data-assets-header', }); + if (isEditPermission) { + await updateOwner({ + page, + owner: owner2[0], + type, + endpoint: this.endpoint, + dataTestId: 'data-assets-header', + }); + await removeOwner({ + page, + endpoint: this.endpoint, + ownerName: owner2[0], + type, + dataTestId: 'data-assets-header', + }); + } + } else { await addMultiOwner({ page, - ownerNames: owner2, + ownerNames: owner1, activatorBtnDataTestId: 'edit-owner', resultTestId: 'data-assets-header', endpoint: this.endpoint, }); - await removeOwner( - page, - this.endpoint, - owner2[0], - type, - 'data-assets-header' - ); + if (isEditPermission) { + await addMultiOwner({ + page, + ownerNames: owner2, + activatorBtnDataTestId: 'edit-owner', + resultTestId: 'data-assets-header', + endpoint: this.endpoint, + }); + await removeOwner({ + page, + endpoint: this.endpoint, + ownerName: owner2[0], + type, + dataTestId: 'data-assets-header', + }); + } } } @@ -195,6 +206,41 @@ export class EntityClass { .isVisible(); } + async tagChildren({ + page, + tag1, + tag2, + rowId, + rowSelector = 'data-row-key', + }: { + page: Page; + tag1: string; + tag2: string; + rowId: string; + rowSelector?: string; + }) { + await assignTagToChildren({ page, tag: tag1, rowId, rowSelector }); + await assignTagToChildren({ + page, + tag: tag2, + rowId, + rowSelector, + action: 'Edit', + }); + await removeTagsFromChildren({ + page, + tags: [tag1, tag2], + rowId, + rowSelector, + }); + + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId('Add') + .isVisible(); + } + async glossaryTerm( page: Page, glossaryTerm1: GlossaryTerm['responseData'], @@ -211,6 +257,46 @@ export class EntityClass { .isVisible(); } + async glossaryTermChildren({ + page, + glossaryTerm1, + glossaryTerm2, + rowId, + rowSelector = 'data-row-key', + }: { + page: Page; + glossaryTerm1: GlossaryTerm['responseData']; + glossaryTerm2: GlossaryTerm['responseData']; + rowId: string; + rowSelector?: string; + }) { + await assignGlossaryTermToChildren({ + page, + glossaryTerm: glossaryTerm1, + rowId, + rowSelector, + }); + await assignGlossaryTermToChildren({ + page, + glossaryTerm: glossaryTerm2, + rowId, + rowSelector, + action: 'Edit', + }); + await removeGlossaryTermFromChildren({ + page, + glossaryTerms: [glossaryTerm1, glossaryTerm2], + rowId, + rowSelector, + }); + + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId('Add') + .isVisible(); + } + async upVote(page: Page) { await upVote(page, this.endpoint); } @@ -265,26 +351,6 @@ export class EntityClass { await hardDeleteEntity(page, displayName ?? entityName, this.endpoint); } - async setCustomProperty( - page: Page, - propertydetails: CustomProperty, - value: string - ) { - await setValueForProperty({ - page, - propertyName: propertydetails.name, - value, - propertyType: propertydetails.propertyType.name, - endpoint: this.endpoint, - }); - await validateValueForProperty({ - page, - propertyName: propertydetails.name, - value, - propertyType: propertydetails.propertyType.name, - }); - } - async updateCustomProperty( page: Page, propertydetails: CustomProperty, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/MlModelClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/MlModelClass.ts index 4ccad160d373..f9f6a6d6693e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/MlModelClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/MlModelClass.ts @@ -38,18 +38,21 @@ export class MlModelClass extends EntityClass { }, }, }; + + children = [ + { + name: 'sales', + dataType: 'numerical', + description: 'Sales amount', + }, + ]; + entity = { name: `pw-mlmodel-${uuid()}`, displayName: `pw-mlmodel-${uuid()}`, service: this.service.name, algorithm: 'Time Series', - mlFeatures: [ - { - name: 'sales', - dataType: 'numerical', - description: 'Sales amount', - }, - ], + mlFeatures: this.children, }; serviceResponseData: ResponseDataType; @@ -59,6 +62,8 @@ export class MlModelClass extends EntityClass { super(EntityTypeEndpoint.MlModel); this.service.name = name ?? this.service.name; this.type = 'MlModel'; + this.childrenTabId = 'features'; + this.childrenSelectorId = `feature-card-${this.children[0].name}`; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/PipelineClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/PipelineClass.ts index 4c830e25b224..e3fe80275e1b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/PipelineClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/PipelineClass.ts @@ -30,11 +30,14 @@ export class PipelineClass extends EntityClass { }, }, }; + + children = [{ name: 'snowflake_task' }]; + entity = { name: `pw-pipeline-${uuid()}`, displayName: `pw-pipeline-${uuid()}`, service: this.service.name, - tasks: [{ name: 'snowflake_task' }], + tasks: this.children, }; serviceResponseData: unknown; @@ -44,6 +47,8 @@ export class PipelineClass extends EntityClass { super(EntityTypeEndpoint.Pipeline); this.service.name = name ?? this.service.name; this.type = 'Pipeline'; + this.childrenTabId = 'tasks'; + this.childrenSelectorId = this.children[0].name; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/SearchIndexClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/SearchIndexClass.ts index 40985e9e9622..7400c73363b2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/SearchIndexClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/SearchIndexClass.ts @@ -33,11 +33,59 @@ export class SearchIndexClass extends EntityClass { }, }, }; + private searchIndexName = `pw-search-index-${uuid()}`; + private fqn = `${this.service.name}.${this.searchIndexName}`; + + children = [ + { + name: 'name', + dataType: 'TEXT', + dataTypeDisplay: 'text', + description: 'Table Entity Name.', + fullyQualifiedName: `${this.fqn}.name`, + tags: [], + }, + { + name: 'description', + dataType: 'TEXT', + dataTypeDisplay: 'text', + description: 'Table Entity Description.', + fullyQualifiedName: `${this.fqn}.description`, + tags: [], + }, + { + name: 'columns', + dataType: 'NESTED', + dataTypeDisplay: 'nested', + description: 'Table Columns.', + fullyQualifiedName: `${this.fqn}.columns`, + tags: [], + children: [ + { + name: 'name', + dataType: 'TEXT', + dataTypeDisplay: 'text', + description: 'Column Name.', + fullyQualifiedName: `${this.fqn}.columns.name`, + tags: [], + }, + { + name: 'description', + dataType: 'TEXT', + dataTypeDisplay: 'text', + description: 'Column Description.', + fullyQualifiedName: `${this.fqn}.columns.description`, + tags: [], + }, + ], + }, + ]; + entity = { - name: `pw-search-index-${uuid()}`, + name: this.searchIndexName, displayName: `pw-search-index-${uuid()}`, service: this.service.name, - fields: [], + fields: this.children, }; serviceResponseData: unknown; @@ -47,6 +95,8 @@ export class SearchIndexClass extends EntityClass { super(EntityTypeEndpoint.SearchIndex); this.service.name = name ?? this.service.name; this.type = 'SearchIndex'; + this.childrenTabId = 'fields'; + this.childrenSelectorId = this.children[0].fullyQualifiedName; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts index 281d86aa6296..50d3f83e2887 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts @@ -44,54 +44,56 @@ export class TableClass extends EntityClass { name: `pw-database-schema-${uuid()}`, database: `${this.service.name}.${this.database.name}`, }; + children = [ + { + 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.', + }, + ]; + entity = { name: `pw-table-${uuid()}`, description: 'description', - 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.', - }, - ], + columns: this.children, databaseSchema: `${this.service.name}.${this.database.name}.${this.schema.name}`, }; @@ -107,6 +109,8 @@ export class TableClass extends EntityClass { super(EntityTypeEndpoint.Table); this.service.name = name ?? this.service.name; this.type = 'Table'; + this.childrenTabId = 'schema'; + this.childrenSelectorId = `${this.entity.databaseSchema}.${this.entity.name}.${this.children[0].name}`; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TopicClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TopicClass.ts index 4157c66e2865..a581a17778ab 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TopicClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TopicClass.ts @@ -33,57 +33,60 @@ export class TopicClass extends EntityClass { }; private topicName = `pw-topic-${uuid()}`; private fqn = `${this.service.name}.${this.topicName}`; - entity = { - name: this.topicName, - service: this.service.name, - 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: [ + + children = [ + { + name: 'default', + dataType: 'RECORD', + fullyQualifiedName: `${this.fqn}.default`, + tags: [], + children: [ { - name: 'default', + name: 'name', dataType: 'RECORD', - fullyQualifiedName: `${this.fqn}.default`, + fullyQualifiedName: `${this.fqn}.default.name`, tags: [], children: [ { - name: 'name', - dataType: 'RECORD', - fullyQualifiedName: `${this.fqn}.default.name`, - tags: [], - children: [ - { - name: 'first_name', - dataType: 'STRING', - description: 'Description for schema field first_name', - fullyQualifiedName: `${this.fqn}.default.name.first_name`, - tags: [], - }, - { - name: 'last_name', - dataType: 'STRING', - fullyQualifiedName: `${this.fqn}.default.name.last_name`, - tags: [], - }, - ], - }, - { - name: 'age', - dataType: 'INT', - fullyQualifiedName: `${this.fqn}.default.age`, + name: 'first_name', + dataType: 'STRING', + description: 'Description for schema field first_name', + fullyQualifiedName: `${this.fqn}.default.name.first_name`, tags: [], }, { - name: 'club_name', + name: 'last_name', dataType: 'STRING', - fullyQualifiedName: `${this.fqn}.default.club_name`, + fullyQualifiedName: `${this.fqn}.default.name.last_name`, tags: [], }, ], }, + { + name: 'age', + dataType: 'INT', + fullyQualifiedName: `${this.fqn}.default.age`, + tags: [], + }, + { + name: 'club_name', + dataType: 'STRING', + fullyQualifiedName: `${this.fqn}.default.club_name`, + tags: [], + }, ], }, + ]; + + entity = { + name: this.topicName, + service: this.service.name, + 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: this.children, + }, partitions: 128, }; @@ -94,6 +97,8 @@ export class TopicClass extends EntityClass { super(EntityTypeEndpoint.Topic); this.service.name = name ?? this.service.name; this.type = 'Topic'; + this.childrenTabId = 'schema'; + this.childrenSelectorId = this.children[0].name; } async create(apiContext: APIRequestContext) { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts index 99edb6080d5f..11ea50d31b0b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/domain.ts @@ -137,14 +137,14 @@ const fillCommonFormItems = async ( await page.click('[data-testid="add-owner"]'); if (!isEmpty(entity.owners) && !isUndefined(entity.owners)) { - await addOwner( + await addOwner({ page, - entity.owners[0].name, - entity.owners[0].type as 'Users' | 'Teams', - EntityTypeEndpoint.Domain, - 'owner-container', - 'add-owner' - ); + owner: entity.owners[0].name, + type: entity.owners[0].type as 'Users' | 'Teams', + endpoint: EntityTypeEndpoint.Domain, + dataTestId: 'owner-container', + initiatorId: 'add-owner', + }); } }; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index 3cf4d64e6575..8ee2618c99a7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -41,14 +41,21 @@ export const visitEntityPage = async (data: { await page.getByTestId('searchBox').clear(); }; -export const addOwner = async ( - page: Page, - owner: string, - type: 'Teams' | 'Users' = 'Users', - endpoint: EntityTypeEndpoint, - dataTestId?: string, - initiatorId = 'edit-owner' -) => { +export const addOwner = async ({ + page, + owner, + endpoint, + type = 'Users', + dataTestId, + initiatorId = 'edit-owner', +}: { + page: Page; + owner: string; + endpoint: EntityTypeEndpoint; + type?: 'Teams' | 'Users'; + dataTestId?: string; + initiatorId?: string; +}) => { await page.getByTestId(initiatorId).click(); if (type === 'Users') { const userListResponse = page.waitForResponse( @@ -91,13 +98,19 @@ export const addOwner = async ( ); }; -export const updateOwner = async ( - page: Page, - owner: string, - type: 'Teams' | 'Users' = 'Users', - endpoint: EntityTypeEndpoint, - dataTestId?: string -) => { +export const updateOwner = async ({ + page, + owner, + endpoint, + type = 'Users', + dataTestId, +}: { + page: Page; + owner: string; + endpoint: EntityTypeEndpoint; + type?: 'Teams' | 'Users'; + dataTestId?: string; +}) => { await page.getByTestId('edit-owner').click(); await page.getByRole('tab', { name: type }).click(); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); @@ -125,13 +138,19 @@ export const updateOwner = async ( ); }; -export const removeOwner = async ( - page: Page, - endpoint: EntityTypeEndpoint, - ownerName: string, - type: 'Teams' | 'Users' = 'Users', - dataTestId?: string -) => { +export const removeOwner = async ({ + page, + endpoint, + ownerName, + type = 'Users', + dataTestId, +}: { + page: Page; + endpoint: EntityTypeEndpoint; + ownerName: string; + type?: 'Teams' | 'Users'; + dataTestId?: string; +}) => { await page.getByTestId('edit-owner').click(); await page.waitForSelector('[data-testid="loader"]', { state: 'detached' }); @@ -196,13 +215,18 @@ export const addMultiOwner = async (data: { await page.getByRole('listitem', { name: ownerName, exact: true }).click(); } + let updateCall = Promise.resolve({}); + + if (!isSelectableInsideForm) { + updateCall = page.waitForResponse(`/api/v1/${endpoint}/*`); + } if (isMultipleOwners) { await page.click('[data-testid="selectable-list-update-btn"]'); } if (!isSelectableInsideForm) { - await page.waitForResponse(`/api/v1/${endpoint}/*`); + await updateCall; } for (const name of owners) { @@ -253,10 +277,11 @@ export const assignTag = async ( .getByTestId(action === 'Add' ? 'add-tag' : 'edit-button') .click(); - await page.locator('#tagsForm_tags').fill(tag); - await page.waitForResponse( + const searchTags = page.waitForResponse( `/api/v1/search/query?q=*${encodeURIComponent(tag)}*` ); + await page.locator('#tagsForm_tags').fill(tag); + await searchTags; await page.getByTestId(`tag-${tag}`).click(); await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); @@ -271,6 +296,53 @@ export const assignTag = async ( ).toBeVisible(); }; +export const assignTagToChildren = async ({ + page, + tag, + rowId, + action = 'Add', + rowSelector = 'data-row-key', +}: { + page: Page; + tag: string; + rowId: string; + action?: 'Add' | 'Edit'; + rowSelector?: string; +}) => { + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId(action === 'Add' ? 'add-tag' : 'edit-button') + .click(); + + const searchTags = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(tag)}*` + ); + + await page.locator('#tagsForm_tags').fill(tag); + + await searchTags; + + await page.getByTestId(`tag-${tag}`).click(); + + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' + ); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + + await patchRequest; + + await expect( + page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId(`tag-${tag}`) + ).toBeVisible(); +}; + export const removeTag = async (page: Page, tags: string[]) => { for (const tag of tags) { await page @@ -285,8 +357,8 @@ export const removeTag = async (page: Page, tags: string[]) => { .locator('svg') .click(); - const patchRequest = page.waitForRequest( - (request) => request.method() === 'PATCH' + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' ); await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); @@ -303,6 +375,49 @@ export const removeTag = async (page: Page, tags: string[]) => { } }; +export const removeTagsFromChildren = async ({ + page, + rowId, + tags, + rowSelector = 'data-row-key', +}: { + page: Page; + tags: string[]; + rowId: string; + rowSelector?: string; +}) => { + for (const tag of tags) { + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId('edit-button') + .click(); + + await page + .getByTestId('tag-selector') + .getByTestId(`selected-tag-${tag}`) + .getByTestId('remove-tags') + .click(); + + const patchTagRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' + ); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + + await patchTagRequest; + + await expect( + page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('tags-container') + .getByTestId(`tag-${tag}`) + ).not.toBeVisible(); + } +}; + type GlossaryTermOption = { displayName: string; name: string; @@ -320,10 +435,12 @@ export const assignGlossaryTerm = async ( .getByTestId(action === 'Add' ? 'add-tag' : 'edit-button') .click(); - await page.locator('#tagsForm_tags').fill(glossaryTerm.displayName); - await page.waitForResponse( + const searchGlossaryTerm = page.waitForResponse( `/api/v1/search/query?q=*${encodeURIComponent(glossaryTerm.displayName)}*` ); + + await page.locator('#tagsForm_tags').fill(glossaryTerm.displayName); + await searchGlossaryTerm; await page.getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`).click(); await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); @@ -338,6 +455,50 @@ export const assignGlossaryTerm = async ( ).toBeVisible(); }; +export const assignGlossaryTermToChildren = async ({ + page, + glossaryTerm, + action = 'Add', + rowId, + rowSelector = 'data-row-key', +}: { + page: Page; + glossaryTerm: GlossaryTermOption; + rowId: string; + action?: 'Add' | 'Edit'; + rowSelector?: string; +}) => { + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId(action === 'Add' ? 'add-tag' : 'edit-button') + .click(); + + const searchGlossaryTerm = page.waitForResponse( + `/api/v1/search/query?q=*${encodeURIComponent(glossaryTerm.displayName)}*` + ); + await page.locator('#tagsForm_tags').fill(glossaryTerm.displayName); + await searchGlossaryTerm; + await page.getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`).click(); + + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' + ); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + + await patchRequest; + + await expect( + page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId(`tag-${glossaryTerm.fullyQualifiedName}`) + ).toBeVisible(); +}; + export const removeGlossaryTerm = async ( page: Page, glossaryTerms: GlossaryTermOption[] @@ -356,8 +517,8 @@ export const removeGlossaryTerm = async ( .locator('svg') .click(); - const patchRequest = page.waitForRequest( - (request) => request.method() === 'PATCH' + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' ); await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); @@ -374,6 +535,50 @@ export const removeGlossaryTerm = async ( } }; +export const removeGlossaryTermFromChildren = async ({ + page, + glossaryTerms, + rowId, + rowSelector = 'data-row-key', +}: { + page: Page; + glossaryTerms: GlossaryTermOption[]; + rowId: string; + rowSelector?: string; +}) => { + for (const tag of glossaryTerms) { + await page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId('edit-button') + .click(); + + await page + .getByTestId('glossary-container') + .getByTestId(new RegExp(tag.name)) + .getByTestId('remove-tags') + .locator('svg') + .click(); + + const patchRequest = page.waitForResponse( + (response) => response.request().method() === 'PATCH' + ); + + await expect(page.getByTestId('saveAssociatedTag')).toBeEnabled(); + + await page.getByTestId('saveAssociatedTag').click(); + + await patchRequest; + + expect( + page + .locator(`[${rowSelector}="${rowId}"]`) + .getByTestId('glossary-container') + .getByTestId(`tag-${tag.fullyQualifiedName}`) + ).not.toBeVisible(); + } +}; + export const upVote = async (page: Page, endPoint: string) => { await page.getByTestId('up-vote-btn').click(); await page.waitForResponse(`/api/v1/${endPoint}/*/vote`); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx index c84df7862686..500cc4fa27f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModels/DataModelDetails.component.tsx @@ -317,7 +317,7 @@ const DataModelDetails = ({ label: ( ), diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx index 36f34d4f6e31..4c68b747df0c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AlertDetailsPage/AlertDetailsPage.tsx @@ -315,6 +315,9 @@ function AlertDetailsPage({