diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/glossaryImportExport.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/glossaryImportExport.ts new file mode 100644 index 000000000000..54d6db070312 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/glossaryImportExport.ts @@ -0,0 +1,21 @@ +export const CUSTOM_PROPERTIES_TYPES = { + STRING: 'String', + MARKDOWN: 'Markdown', + SQL_QUERY: 'Sql Query', + ENUM_WITH_DESCRIPTIONS: 'Enum With Descriptions', +}; + +export const FIELD_VALUES_CUSTOM_PROPERTIES = { + STRING: 'This is "testing" string;', + MARKDOWN: `## Overview +This project is designed to **simplify** and *automate* daily tasks. It aims to: +- Increase productivity +- Reduce manual effort +- Provide real-time data insights + +## Features +1. **Task Management**: Organize tasks efficiently with custom tags. +2. **Real-Time Analytics**: Get up-to-date insights on task progress. +3. **Automation**: Automate repetitive workflows using custom scripts.`, + SQL_QUERY: 'SELECT * FROM table_name WHERE id="20";', +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryImportExport.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryImportExport.spec.ts index b93c554629a9..c28eeba14f22 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryImportExport.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/GlossaryImportExport.spec.ts @@ -11,6 +11,9 @@ * limitations under the License. */ import { expect, test } from '@playwright/test'; +import { CUSTOM_PROPERTIES_ENTITIES } from '../../constant/customProperty'; +import { CUSTOM_PROPERTIES_TYPES } from '../../constant/glossaryImportExport'; +import { GlobalSettingOptions } from '../../constant/settings'; import { SidebarItem } from '../../constant/sidebar'; import { Glossary } from '../../support/glossary/Glossary'; import { GlossaryTerm } from '../../support/glossary/GlossaryTerm'; @@ -19,14 +22,19 @@ import { createNewPage, redirectToHomePage, toastNotification, + uuid, } from '../../utils/common'; +import { + addCustomPropertiesForEntity, + deleteCreatedProperty, +} from '../../utils/customProperty'; import { selectActiveGlossary } from '../../utils/glossary'; import { createGlossaryTermRowDetails, fillGlossaryRowDetails, validateImportStatus, } from '../../utils/importUtils'; -import { sidebarClick } from '../../utils/sidebar'; +import { settingClick, sidebarClick } from '../../utils/sidebar'; // use the admin user to login test.use({ @@ -39,6 +47,9 @@ const glossary1 = new Glossary(); const glossary2 = new Glossary(); const glossaryTerm1 = new GlossaryTerm(glossary1); const glossaryTerm2 = new GlossaryTerm(glossary2); +const propertiesList = Object.values(CUSTOM_PROPERTIES_TYPES); + +const propertyListName: Record = {}; test.describe('Glossary Bulk Import Export', () => { test.slow(true); @@ -72,6 +83,24 @@ test.describe('Glossary Bulk Import Export', () => { }); test('Glossary Bulk Import Export', async ({ page }) => { + await test.step('create custom properties for extension edit', async () => { + for (const property of propertiesList) { + const entity = CUSTOM_PROPERTIES_ENTITIES.entity_glossaryTerm; + const propertyName = `pwcustomproperty${entity.name}test${uuid()}`; + propertyListName[property] = propertyName; + + await settingClick(page, GlobalSettingOptions.GLOSSARY_TERM, true); + + await addCustomPropertiesForEntity({ + page, + propertyName, + customPropertyData: entity, + customType: property, + enumWithDescriptionConfig: entity.enumWithDescriptionConfig, + }); + } + }); + await test.step('should export data glossary term details', async () => { await sidebarClick(page, SidebarItem.GLOSSARY); await selectActiveGlossary(page, glossary1.data.displayName); @@ -89,7 +118,7 @@ test.describe('Glossary Bulk Import Export', () => { }); await test.step( - 'should import and edit with two additional database', + 'should import and edit with one additional glossaryTerm', async () => { await sidebarClick(page, SidebarItem.GLOSSARY); await selectActiveGlossary(page, glossary1.data.displayName); @@ -130,7 +159,8 @@ test.describe('Glossary Bulk Import Export', () => { name: glossaryTerm2.data.name, }, }, - page + page, + propertyListName ); await page.getByRole('button', { name: 'Next' }).click(); @@ -167,5 +197,17 @@ test.describe('Glossary Bulk Import Export', () => { ); } ); + + await test.step('delete custom properties', async () => { + for (const propertyName of Object.values(propertyListName)) { + await settingClick(page, GlobalSettingOptions.GLOSSARY_TERM, true); + + await page.waitForLoadState('networkidle'); + + await page.getByTestId('loader').waitFor({ state: 'detached' }); + + await deleteCreatedProperty(page, propertyName); + } + }); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/importUtils.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/importUtils.ts index e54e056a5675..1003eda7f7bf 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/importUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/importUtils.ts @@ -11,7 +11,12 @@ * limitations under the License. */ import { expect, Page } from '@playwright/test'; -import { uuid } from './common'; +import { CUSTOM_PROPERTIES_ENTITIES } from '../constant/customProperty'; +import { + CUSTOM_PROPERTIES_TYPES, + FIELD_VALUES_CUSTOM_PROPERTIES, +} from '../constant/glossaryImportExport'; +import { descriptionBox, uuid } from './common'; export const createGlossaryTermRowDetails = () => { return { @@ -129,6 +134,100 @@ export const fillDomainDetails = async ( await page.waitForTimeout(100); }; +const editGlossaryCustomProperty = async ( + page: Page, + propertyName: string, + type: string +) => { + await page + .locator( + `[data-testid=${propertyName}] [data-testid='edit-icon-right-panel']` + ) + .click(); + + if (type === CUSTOM_PROPERTIES_TYPES.STRING) { + await page + .getByTestId('value-input') + .fill(FIELD_VALUES_CUSTOM_PROPERTIES.STRING); + await page.getByTestId('inline-save-btn').click(); + } + + if (type === CUSTOM_PROPERTIES_TYPES.MARKDOWN) { + await page.waitForSelector(descriptionBox, { state: 'visible' }); + + await page + .locator(descriptionBox) + .fill(FIELD_VALUES_CUSTOM_PROPERTIES.MARKDOWN); + + await page.getByTestId('markdown-editor').getByTestId('save').click(); + + await page.waitForSelector(descriptionBox, { + state: 'detached', + }); + } + + if (type === CUSTOM_PROPERTIES_TYPES.SQL_QUERY) { + await page + .getByTestId('code-mirror-container') + .getByRole('textbox') + .fill(FIELD_VALUES_CUSTOM_PROPERTIES.SQL_QUERY); + + await page.getByTestId('inline-save-btn').click(); + } + + if (type === CUSTOM_PROPERTIES_TYPES.ENUM_WITH_DESCRIPTIONS) { + await page.getByTestId('enum-with-description-select').click(); + + await page.waitForSelector('.ant-select-dropdown', { + state: 'visible', + }); + + // await page + // .getByRole('option', { + // name: CUSTOM_PROPERTIES_ENTITIES.entity_glossaryTerm + // .enumWithDescriptionConfig.values[0].key, + // }) + // .click(); + + await page + .locator('span') + .filter({ + hasText: + CUSTOM_PROPERTIES_ENTITIES.entity_glossaryTerm + .enumWithDescriptionConfig.values[0].key, + }) + .click(); + + await page.getByTestId('inline-save-btn').click(); + } +}; + +export const fillCustomPropertyDetails = async ( + page: Page, + propertyListName: Record +) => { + await page + .locator('.InovuaReactDataGrid__cell--cell-active') + .press('Enter', { delay: 100 }); + + // Wait for the loader to disappear + await page.waitForSelector('.ant-skeleton-content', { state: 'hidden' }); + + for (const propertyName of Object.values(CUSTOM_PROPERTIES_TYPES)) { + await editGlossaryCustomProperty( + page, + propertyListName[propertyName], + propertyName + ); + } + + await page.getByTestId('save').click(); + + await expect(page.locator('.ant-modal-wrap')).not.toBeVisible(); + + await page.click('.InovuaReactDataGrid__cell--cell-active'); +}; + export const fillGlossaryRowDetails = async ( row: { name: string; @@ -144,7 +243,8 @@ export const fillGlossaryRowDetails = async ( reviewers: string[]; owners: string[]; }, - page: Page + page: Page, + propertyListName: Record ) => { await page .locator('.InovuaReactDataGrid__cell--cell-active') @@ -200,6 +300,12 @@ export const fillGlossaryRowDetails = async ( .press('ArrowRight', { delay: 100 }); await fillOwnerDetails(page, row.owners); + + await page + .locator('.InovuaReactDataGrid__cell--cell-active') + .press('ArrowRight', { delay: 100 }); + + await fillCustomPropertyDetails(page, propertyListName); }; export const validateImportStatus = async ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ModalWithCustomProperty/ModalWithCustomPropertyEditor.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ModalWithCustomProperty/ModalWithCustomPropertyEditor.component.tsx index 1a56cb8e5978..567bbf2da9da 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/ModalWithCustomProperty/ModalWithCustomPropertyEditor.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/ModalWithCustomProperty/ModalWithCustomPropertyEditor.component.tsx @@ -11,13 +11,14 @@ * limitations under the License. */ import { Button, Modal, Typography } from 'antd'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { AxiosError } from 'axios'; +import { isObject } from 'lodash'; import { EntityType } from '../../../enums/entity.enum'; import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; -import { Type } from '../../../generated/entity/type'; +import { EnumConfig, Type, ValueClass } from '../../../generated/entity/type'; import { getTypeByFQN } from '../../../rest/metadataTypeAPI'; import { convertCustomPropertyStringToEntityExtension, @@ -46,6 +47,20 @@ export const ModalWithCustomPropertyEditor = ({ useState(); const [customPropertyTypes, setCustomPropertyTypes] = useState(); + const enumWithDescriptionsKeyPairValues = useMemo(() => { + const valuesWithEnumKey: Record = {}; + + customPropertyTypes?.customProperties?.forEach((property) => { + if (property.propertyType.name === 'enumWithDescriptions') { + valuesWithEnumKey[property.name] = ( + property.customPropertyConfig?.config as EnumConfig + ).values as ValueClass[]; + } + }); + + return valuesWithEnumKey; + }, [customPropertyTypes]); + const fetchTypeDetail = async () => { setIsLoading(true); try { @@ -72,8 +87,42 @@ export const ModalWithCustomPropertyEditor = ({ setIsSaveLoading(false); }; + // EnumWithDescriptions values are change only contain keys, + // so we need to modify the extension data to include descriptions for them to display in the table + const modifyExtensionData = useCallback( + (extension: ExtensionDataProps) => { + const modifiedExtension = Object.entries(extension).reduce( + (acc, [key, value]) => { + if (enumWithDescriptionsKeyPairValues[key]) { + return { + ...acc, + [key]: (value as string[] | ValueClass[]).map((item) => { + if (isObject(item)) { + return item; + } + + return { + key: item, + description: enumWithDescriptionsKeyPairValues[key].find( + (val) => val.key === item + )?.description, + }; + }), + }; + } + + return { ...acc, [key]: value }; + }, + {} + ); + + return modifiedExtension; + }, + [enumWithDescriptionsKeyPairValues] + ); + const onExtensionUpdate = async (data: GlossaryTerm) => { - setCustomPropertyValue(data.extension); + setCustomPropertyValue(modifyExtensionData(data.extension)); }; useEffect(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/PropertyValue.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/PropertyValue.tsx index 72add4ef09e4..ab0eb32376d6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/PropertyValue.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/PropertyValue.tsx @@ -1002,7 +1002,7 @@ export const PropertyValue: FC = ({ }, [property, extension, contentRef, value]); const customPropertyElement = ( - + 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 264eba154b4c..ac20fe39f757 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -934,5 +934,5 @@ export const removeOuterEscapes = (input: string) => { const match = input.match(VALIDATE_ESCAPE_START_END_REGEX); // Return the middle part without the outer escape characters or the original input if no match - return match ? match[2] : input; + return match && match.length > 3 ? match[2] : input; };