From a034a979f18b189e8fb15693c532acd66405cf1f Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 28 Jun 2024 11:10:47 +0530 Subject: [PATCH] Fix: #16742 - allow all the tier tags to be assign on entity (#16823) * fix(ui): consider all the tags as tier which are children of Tier classification * add playwright * wip tier cleanup * fix async select list loading issue * address comments * update variable --- .../ui/playwright/e2e/Pages/Entity.spec.ts | 6 +- .../support/entity/EntityDataClass.ts | 4 + .../ui/playwright/support/tag/TagClass.ts | 94 +++++++++++++++++++ .../main/resources/ui/playwright/utils/tag.ts | 29 ++++++ .../AsyncSelectList/AsyncSelectList.tsx | 2 +- .../resources/ui/src/utils/CommonUtils.tsx | 4 +- .../resources/ui/src/utils/EntityUtils.tsx | 38 ++++---- .../ui/src/utils/TableUtils.test.tsx | 43 +++++++++ .../resources/ui/src/utils/TableUtils.tsx | 22 +---- 9 files changed, 200 insertions(+), 42 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/support/tag/TagClass.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/utils/tag.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.test.tsx 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 bb3f322d4d4..6f40658bbe4 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 @@ -85,7 +85,11 @@ entities.forEach((EntityClass) => { }); test('Tier Add, Update and Remove', async ({ page }) => { - await entity.tier(page, 'Tier1', 'Tier5'); + await entity.tier( + page, + 'Tier1', + EntityDataClass.tierTag1.data.displayName + ); }); test('Update description', async ({ page }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityDataClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityDataClass.ts index 750c143aa04..ee852948afb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityDataClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/EntityDataClass.ts @@ -14,6 +14,7 @@ import { APIRequestContext } from '@playwright/test'; import { Domain } from '../domain/Domain'; import { Glossary } from '../glossary/Glossary'; import { GlossaryTerm } from '../glossary/GlossaryTerm'; +import { TagClass } from '../tag/TagClass'; import { TeamClass } from '../team/TeamClass'; import { UserClass } from '../user/UserClass'; @@ -28,6 +29,7 @@ export class EntityDataClass { static readonly user2 = new UserClass(); static readonly team1 = new TeamClass(); static readonly team2 = new TeamClass(); + static readonly tierTag1 = new TagClass({ classification: 'Tier' }); static async preRequisitesForTests(apiContext: APIRequestContext) { // Add pre-requisites for tests @@ -41,6 +43,7 @@ export class EntityDataClass { await this.user2.create(apiContext); await this.team1.create(apiContext); await this.team2.create(apiContext); + await this.tierTag1.create(apiContext); } static async postRequisitesForTests(apiContext: APIRequestContext) { @@ -54,5 +57,6 @@ export class EntityDataClass { await this.user2.delete(apiContext); await this.team1.delete(apiContext); await this.team2.delete(apiContext); + await this.tierTag1.delete(apiContext); } } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/tag/TagClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/tag/TagClass.ts new file mode 100644 index 00000000000..c5aa76bbc28 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/tag/TagClass.ts @@ -0,0 +1,94 @@ +/* + * 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 { APIRequestContext, Page } from '@playwright/test'; +import { visitClassificationPage } from '../../utils/tag'; +import { getRandomLastName } from '../../utils/user'; + +type ResponseDataType = { + style?: { + color: string; + }; + description: string; + displayName: string; + classification: { + id: string; + type: 'classification'; + name: string; + fullyQualifiedName: string; + description: string; + displayName: string; + deleted: boolean; + href: string; + }; + name: string; + id: string; + fullyQualifiedName: string; +}; + +export type TagData = { + style?: { + color: string; + }; + description: string; + displayName: string; + classification: string; + name: string; +}; + +export class TagClass { + randomName = getRandomLastName(); + data: TagData = { + name: `pw-tier-${this.randomName}`, + displayName: `PW Tier ${this.randomName}`, + description: 'Tier tag for the Collate platform', + style: { + color: '#FFD700', + }, + classification: 'Tier', + }; + + responseData: ResponseDataType; + + constructor(tag: Partial) { + this.data.classification = tag.classification ?? this.data.classification; + } + + async visitPage(page: Page) { + await visitClassificationPage( + page, + this.responseData.classification.displayName + ); + } + + async create(apiContext: APIRequestContext) { + const response = await apiContext.post('/api/v1/tags', { + data: this.data, + }); + + this.responseData = await response.json(); + + return await response.json(); + } + + get() { + return this.responseData; + } + + async delete(apiContext: APIRequestContext) { + const response = await apiContext.delete( + `/api/v1/tags/${this.responseData.id}?recursive=true&hardDelete=true` + ); + + return await response.json(); + } +} diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/tag.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/tag.ts new file mode 100644 index 00000000000..6d5e32682f1 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/tag.ts @@ -0,0 +1,29 @@ +/* + * 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 { Page } from '@playwright/test'; +import { SidebarItem } from '../constant/sidebar'; +import { redirectToHomePage } from './common'; +import { sidebarClick } from './sidebar'; + +export const visitClassificationPage = async ( + page: Page, + classificationName: string +) => { + await redirectToHomePage(page); + const classificationResponse = page.waitForResponse( + '/api/v1/classifications?**' + ); + await sidebarClick(page, SidebarItem.TAGS); + await classificationResponse; + await page.getByRole('menuitem', { name: classificationName }).click(); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx index ffb560dfdcf..2a36c2ee375 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx @@ -122,7 +122,7 @@ const AsyncSelectList: FC = ({ const tagOptions = useMemo(() => { const newTags = options - .filter((tag) => !tag.label?.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`)) // To filter out Tier tags + .filter((tag) => !tag.label?.startsWith(`Tier${FQN_SEPARATOR_CHAR}`)) // To filter out Tier tags .map((tag) => { const displayName = tag.data?.displayName; const parts = Fqn.split(tag.label); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx index 95f07a548dd..ee0b777e272 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -688,13 +688,13 @@ export const getEntityIdArray = (entities: EntityReference[]): string[] => export const getTagValue = (tag: string | TagLabel): string | TagLabel => { if (isString(tag)) { - return tag.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`) + return tag.startsWith(`Tier${FQN_SEPARATOR_CHAR}`) ? tag.split(FQN_SEPARATOR_CHAR)[1] : tag; } else { return { ...tag, - tagFQN: tag.tagFQN.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`) + tagFQN: tag.tagFQN.startsWith(`Tier${FQN_SEPARATOR_CHAR}`) ? tag.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : tag.tagFQN, }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx index 49ab753ccd7..805c845c10a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx @@ -135,7 +135,7 @@ import { stringToHTML } from './StringsUtils'; import { getDataTypeString, getTagsWithoutTier, - getTierFromTableTags, + getTierTags, getUsagePercentile, } from './TableUtils'; import { getTableTags } from './TagsUtils'; @@ -217,7 +217,7 @@ const getTableFieldsFromTableDetails = (tableDetails: Table) => { const databaseDisplayName = getEntityName(database) || databaseName; const schemaDisplayName = getEntityName(databaseSchema) || schemaName; - const tier = getTierFromTableTags(tags ?? []); + const tier = getTierTags(tags ?? []); return { fullyQualifiedName, @@ -301,7 +301,7 @@ const getTableOverview = (tableDetails: Table) => { }, { name: i18next.t('label.tier'), - value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + value: tier ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, isLink: false, visible: [DRAWER_NAVIGATION_OPTIONS.lineage], }, @@ -343,7 +343,7 @@ const getTableOverview = (tableDetails: Table) => { const getPipelineOverview = (pipelineDetails: Pipeline) => { const { owner, tags, sourceUrl, service, displayName } = pipelineDetails; - const tier = getTierFromTableTags(tags ?? []); + const tier = getTierTags(tags ?? []); const serviceDisplayName = getEntityName(service); const overview = [ @@ -381,7 +381,7 @@ const getPipelineOverview = (pipelineDetails: Pipeline) => { }, { name: i18next.t('label.tier'), - value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + value: tier ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, isLink: false, visible: [DRAWER_NAVIGATION_OPTIONS.lineage], }, @@ -393,7 +393,7 @@ const getPipelineOverview = (pipelineDetails: Pipeline) => { const getDashboardOverview = (dashboardDetails: Dashboard) => { const { owner, tags, sourceUrl, service, displayName, project } = dashboardDetails; - const tier = getTierFromTableTags(tags ?? []); + const tier = getTierTags(tags ?? []); const serviceDisplayName = getEntityName(service); const overview = [ @@ -430,7 +430,7 @@ const getDashboardOverview = (dashboardDetails: Dashboard) => { }, { name: i18next.t('label.tier'), - value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + value: tier ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, isLink: false, isExternal: false, visible: [DRAWER_NAVIGATION_OPTIONS.lineage], @@ -453,7 +453,7 @@ export const getSearchIndexOverview = ( searchIndexDetails: SearchIndexEntity ) => { const { owner, tags, service } = searchIndexDetails; - const tier = getTierFromTableTags(tags ?? []); + const tier = getTierTags(tags ?? []); const overview = [ { @@ -465,7 +465,7 @@ export const getSearchIndexOverview = ( }, { name: i18next.t('label.tier'), - value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + value: tier ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, isLink: false, isExternal: false, visible: [DRAWER_NAVIGATION_OPTIONS.lineage], @@ -591,7 +591,7 @@ const getDataModelOverview = (dataModelDetails: DashboardDataModel) => { dataModelType, fullyQualifiedName, } = dataModelDetails; - const tier = getTierFromTableTags(tags ?? []); + const tier = getTierTags(tags ?? []); const overview = [ { @@ -633,7 +633,7 @@ const getDataModelOverview = (dataModelDetails: DashboardDataModel) => { { name: i18next.t('label.tier'), - value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + value: tier ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, isLink: false, isExternal: false, visible: [ @@ -667,7 +667,7 @@ const getStoredProcedureOverview = ( FQN_SEPARATOR_CHAR ).split(FQN_SEPARATOR_CHAR); - const tier = getTierFromTableTags(tags ?? []); + const tier = getTierTags(tags ?? []); const overview = [ { @@ -717,7 +717,7 @@ const getStoredProcedureOverview = ( }, { name: i18next.t('label.tier'), - value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + value: tier ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, isLink: false, visible: [ DRAWER_NAVIGATION_OPTIONS.lineage, @@ -747,7 +747,7 @@ const getStoredProcedureOverview = ( const getDatabaseOverview = (databaseDetails: Database) => { const { owner, service, tags, usageSummary } = databaseDetails; - const tier = getTierFromTableTags(tags ?? []); + const tier = getTierTags(tags ?? []); const overview = [ { @@ -760,7 +760,7 @@ const getDatabaseOverview = (databaseDetails: Database) => { { name: i18next.t('label.tier'), - value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + value: tier ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, isLink: false, visible: [DRAWER_NAVIGATION_OPTIONS.explore], }, @@ -790,7 +790,7 @@ const getDatabaseSchemaOverview = (databaseSchemaDetails: DatabaseSchema) => { const { owner, service, tags, usageSummary, database } = databaseSchemaDetails; - const tier = getTierFromTableTags(tags ?? []); + const tier = getTierTags(tags ?? []); const overview = [ { @@ -803,7 +803,7 @@ const getDatabaseSchemaOverview = (databaseSchemaDetails: DatabaseSchema) => { { name: i18next.t('label.tier'), - value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + value: tier ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, isLink: false, visible: [DRAWER_NAVIGATION_OPTIONS.explore], }, @@ -841,7 +841,7 @@ const getDatabaseSchemaOverview = (databaseSchemaDetails: DatabaseSchema) => { const getEntityServiceOverview = (serviceDetails: EntityServiceUnion) => { const { owner, tags, serviceType } = serviceDetails; - const tier = getTierFromTableTags(tags ?? []); + const tier = getTierTags(tags ?? []); const overview = [ { @@ -854,7 +854,7 @@ const getEntityServiceOverview = (serviceDetails: EntityServiceUnion) => { { name: i18next.t('label.tier'), - value: tier ? tier.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, + value: tier ? tier.tagFQN.split(FQN_SEPARATOR_CHAR)[1] : NO_DATA, isLink: false, visible: [DRAWER_NAVIGATION_OPTIONS.explore], }, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.test.tsx new file mode 100644 index 00000000000..2a3c7767794 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.test.tsx @@ -0,0 +1,43 @@ +/* + * 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 { TagLabel } from '../generated/entity/data/container'; +import { getTagsWithoutTier, getTierTags } from '../utils/TableUtils'; + +describe('TableUtils', () => { + it('getTierTags should return the correct usage percentile', () => { + const tags = [ + { tagFQN: 'Tier.Tier1' }, + { tagFQN: 'RandomTag' }, + { tagFQN: 'OtherTag' }, + ] as TagLabel[]; + const result = getTierTags(tags); + + expect(result).toStrictEqual({ tagFQN: 'Tier.Tier1' }); + }); + + it('getTagsWithoutTier should return the tier tag FQN', () => { + const tags = [ + { tagFQN: 'Tier.Gold' }, + { tagFQN: 'RandomTag' }, + { tagFQN: 'OtherTag' }, + ]; + const result = getTagsWithoutTier(tags); + + expect(result).toStrictEqual([ + { tagFQN: 'RandomTag' }, + { tagFQN: 'OtherTag' }, + ]); + }); + + // Add more tests for other functions in TableUtils... +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index d351315d9ee..8220708f32a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -103,23 +103,9 @@ export const getUsagePercentile = (pctRank: number, isLiteral = false) => { return usagePercentile; }; -export const getTierFromTableTags = ( - tags: Array -): EntityTags['tagFQN'] => { - const tierTag = tags.find( - (item) => - item.tagFQN.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`) && - !isNaN(parseInt(item.tagFQN.substring(9).trim())) - ); - - return tierTag?.tagFQN || ''; -}; - export const getTierTags = (tags: Array) => { - const tierTag = tags.find( - (item) => - item.tagFQN.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`) && - !isNaN(parseInt(item.tagFQN.substring(9).trim())) + const tierTag = tags.find((item) => + item.tagFQN.startsWith(`Tier${FQN_SEPARATOR_CHAR}`) ); return tierTag; @@ -129,9 +115,7 @@ export const getTagsWithoutTier = ( tags: Array ): Array => { return tags.filter( - (item) => - !item.tagFQN.startsWith(`Tier${FQN_SEPARATOR_CHAR}Tier`) || - isNaN(parseInt(item.tagFQN.substring(9).trim())) + (item) => !item.tagFQN.startsWith(`Tier${FQN_SEPARATOR_CHAR}`) ); };