From 55cd180ffa31ae4bfae64fd81058552a2afa0d14 Mon Sep 17 00:00:00 2001 From: Sachin Chaurasiya Date: Thu, 13 Jun 2024 20:51:47 +0530 Subject: [PATCH] feat(#16208): custom properties for Dashboard Data Models (#16628) * feat(#16208): custom properties for Dashboard Data Models * chore: update the padding for custom properties tab content * Backend : Support extension field for dashboardDataModel * ui: update ui to fetch the extension data * feat: show custom properties on right panel * chore: add custom properties in version page * test: add playwright test for data model custom properties * test: remove duplicate checks --------- Co-authored-by: sonikashah --- .../DashboardDataModelResource.java | 2 +- .../api/data/createDashboardDataModel.json | 4 ++ .../entity/data/dashboardDataModel.json | 4 ++ .../ui/playwright/constant/customProperty.ts | 1 + .../support/entity/Entity.interface.ts | 1 + .../ui/playwright/utils/customProperty.ts | 26 ---------- .../DataModelVersion.component.tsx | 50 ++++++++++++++++++- .../DataModelVersion.interface.ts | 2 + .../DataModels/DataModelDetails.component.tsx | 48 +++++++++++++++++- .../CustomPropertyTable.interface.ts | 2 + .../src/constants/GlobalSettings.constants.ts | 1 + .../ui/src/constants/PageHeaders.constant.ts | 6 +++ .../resources/ui/src/constants/constants.ts | 1 + .../CustomPropertiesPageV1.tsx | 3 ++ .../DataModelPage/DataModelPage.component.tsx | 4 +- .../EntityVersionPage.component.tsx | 1 + .../ui/src/utils/GlobalSettingsUtils.tsx | 10 ++++ 17 files changed, 135 insertions(+), 31 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java index 2fdf435fe12..9d16580eced 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java @@ -71,7 +71,7 @@ import org.openmetadata.service.util.ResultList; public class DashboardDataModelResource extends EntityResource { public static final String COLLECTION_PATH = "/v1/dashboard/datamodels"; - protected static final String FIELDS = "owner,tags,followers,domain,sourceHash"; + protected static final String FIELDS = "owner,tags,followers,domain,sourceHash,extension"; @Override public DashboardDataModel addHref(UriInfo uriInfo, DashboardDataModel dashboardDataModel) { diff --git a/openmetadata-spec/src/main/resources/json/schema/api/data/createDashboardDataModel.json b/openmetadata-spec/src/main/resources/json/schema/api/data/createDashboardDataModel.json index 5de4830429b..8bedcca0b28 100644 --- a/openmetadata-spec/src/main/resources/json/schema/api/data/createDashboardDataModel.json +++ b/openmetadata-spec/src/main/resources/json/schema/api/data/createDashboardDataModel.json @@ -79,6 +79,10 @@ "type": "string", "minLength": 1, "maxLength": 32 + }, + "extension": { + "description": "Entity extension data with custom attributes added to the entity.", + "$ref": "../../type/basic.json#/definitions/entityExtension" } }, "required": ["name", "service", "dataModelType", "columns"], diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/data/dashboardDataModel.json b/openmetadata-spec/src/main/resources/json/schema/entity/data/dashboardDataModel.json index 11e8a48230e..390e429ad5d 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/data/dashboardDataModel.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/data/dashboardDataModel.json @@ -159,6 +159,10 @@ "type": "string", "minLength": 1, "maxLength": 32 + }, + "extension": { + "description": "Entity extension data with custom attributes added to the entity.", + "$ref": "../../type/basic.json#/definitions/entityExtension" } }, "required": [ diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/customProperty.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/customProperty.ts index 632f3dd8ad2..cbc9c4827bf 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/constant/customProperty.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/customProperty.ts @@ -25,6 +25,7 @@ export const CustomPropertySupportedEntityList = [ EntityTypeEndpoint.MlModel, EntityTypeEndpoint.GlossaryTerm, EntityTypeEndpoint.SearchIndex, + EntityTypeEndpoint.DataModel, ]; export const ENTITY_REFERENCE_PROPERTIES = [ diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/Entity.interface.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/Entity.interface.ts index 0c91f048c5b..32242e5a5d6 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/Entity.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/Entity.interface.ts @@ -56,4 +56,5 @@ export enum ENTITY_PATH { glossaryTerm = 'glossaryTerm', databases = 'database', databaseSchemas = 'databaseSchema', + 'dashboard/datamodels' = 'dashboardDataModel', } diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts index 3c7f68f089a..0c4526c598c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/customProperty.ts @@ -159,32 +159,6 @@ export const setValueForProperty = async (data: { break; } } - - await page.waitForResponse('/api/v1/*/*'); - if (propertyType === 'enum') { - await expect( - page.getByLabel('Custom Properties').getByTestId('enum-value') - ).toContainText(value); - } else if (propertyType === 'timeInterval') { - const [startValue, endValue] = value.split(','); - - await expect( - page.getByLabel('Custom Properties').getByTestId('time-interval-value') - ).toContainText(startValue); - await expect( - page.getByLabel('Custom Properties').getByTestId('time-interval-value') - ).toContainText(endValue); - } else if (propertyType === 'sqlQuery') { - await expect( - page.getByLabel('Custom Properties').locator('.CodeMirror-scroll') - ).toContainText(value); - } else if ( - !['entityReference', 'entityReferenceList'].includes(propertyType) - ) { - await expect(page.getByRole('row', { name: propertyName })).toContainText( - value.replace(/\*|_/gi, '') - ); - } }; export const validateValueForProperty = async (data: { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.component.tsx index f23b9da630c..2f3887a2bbf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.component.tsx @@ -16,7 +16,9 @@ import classNames from 'classnames'; import { cloneDeep } from 'lodash'; import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useHistory, useParams } from 'react-router-dom'; import { FQN_SEPARATOR_CHAR } from '../../../../constants/char.constants'; +import { getVersionPath } from '../../../../constants/constants'; import { EntityField } from '../../../../constants/Feeds.constants'; import { EntityTabs, EntityType, FqnPart } from '../../../../enums/entity.enum'; import { @@ -32,6 +34,7 @@ import { getEntityVersionByField, getEntityVersionTags, } from '../../../../utils/EntityVersionUtils'; +import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; import Loader from '../../../common/Loader/Loader'; import TabsLabel from '../../../common/TabsLabel/TabsLabel.component'; @@ -55,12 +58,20 @@ const DataModelVersion: FC = ({ deleted = false, backHandler, versionHandler, + entityPermissions, }: DataModelVersionProp) => { + const history = useHistory(); const { t } = useTranslation(); + const { tab } = useParams<{ tab: EntityTabs }>(); const [changeDescription, setChangeDescription] = useState( currentVersionData.changeDescription as ChangeDescription ); + const entityFqn = useMemo( + () => currentVersionData.fullyQualifiedName ?? '', + [currentVersionData.fullyQualifiedName] + ); + const { ownerDisplayName, ownerRef, tierDisplayName, domainDisplayName } = useMemo( () => @@ -107,6 +118,17 @@ const DataModelVersion: FC = ({ ); }, [currentVersionData, changeDescription]); + const handleTabChange = (activeKey: string) => { + history.push( + getVersionPath( + EntityType.DASHBOARD_DATA_MODEL, + entityFqn, + String(version), + activeKey + ) + ); + }; + const tabItems: TabsProps['items'] = useMemo( () => [ { @@ -160,8 +182,28 @@ const DataModelVersion: FC = ({ ), }, + { + key: EntityTabs.CUSTOM_PROPERTIES, + label: ( + + ), + children: ( +
+ +
+ ), + }, ], - [description, columns] + [description, columns, currentVersionData, entityPermissions] ); return ( @@ -191,7 +233,11 @@ const DataModelVersion: FC = ({ /> - + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.interface.ts index 6f4eed8de44..59caa843bab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Dashboard/DataModel/DataModelVersion/DataModelVersion.interface.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface'; import { DashboardDataModel } from '../../../../generated/entity/data/dashboardDataModel'; import { EntityHistory } from '../../../../generated/type/entityHistory'; import { TagLabel } from '../../../../generated/type/tagLabel'; @@ -30,4 +31,5 @@ export interface DataModelVersionProp { deleted?: boolean; backHandler: () => void; versionHandler: (v: string) => void; + entityPermissions: OperationPermission; } 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 cbec3997f1c..02d51b20eac 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 @@ -26,6 +26,7 @@ import { FEED_COUNT_INITIAL_DATA } from '../../../../constants/entity.constants' import LineageProvider from '../../../../context/LineageProvider/LineageProvider'; import { CSMode } from '../../../../enums/codemirror.enum'; import { EntityTabs, EntityType } from '../../../../enums/entity.enum'; +import { DashboardDataModel } from '../../../../generated/entity/data/dashboardDataModel'; import { TagLabel } from '../../../../generated/type/tagLabel'; import { useFqn } from '../../../../hooks/useFqn'; import { FeedCounts } from '../../../../interface/feed.interface'; @@ -39,6 +40,7 @@ import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvi import { ActivityFeedTab } from '../../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.component'; import ActivityThreadPanel from '../../../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import { withActivityFeed } from '../../../AppRouter/withActivityFeed'; +import { CustomPropertyTable } from '../../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; import ResizablePanels from '../../../common/ResizablePanels/ResizablePanels'; import TabsLabel from '../../../common/TabsLabel/TabsLabel.component'; @@ -209,6 +211,18 @@ const DataModelDetails = ({ setIsEditDescription(false); }; + const handelExtensionUpdate = useCallback( + async (updatedDataModel: DashboardDataModel) => { + await onUpdateDataModel( + { + ...dataModelData, + extension: updatedDataModel.extension, + }, + 'extension' + ); + }, + [onUpdateDataModel, dataModelData] + ); const modelComponent = useMemo(() => { return ( @@ -251,14 +265,22 @@ const DataModelDetails = ({ secondPanel={{ children: (
- + customProperties={dataModelData} dataProducts={dataModelData?.dataProducts ?? []} domain={dataModelData?.domain} + editCustomAttributePermission={ + (dataModelPermissions.EditAll || + dataModelPermissions.EditCustomFields) && + !deleted + } editTagPermission={editTagsPermission} entityFQN={decodedDataModelFQN} entityId={dataModelData.id} entityType={EntityType.DASHBOARD_DATA_MODEL} selectedTags={tags} + viewAllPermission={dataModelPermissions.ViewAll} + onExtensionUpdate={handelExtensionUpdate} onTagSelectionChange={handleTagSelection} onThreadLinkSelect={onThreadLinkSelect} /> @@ -370,6 +392,30 @@ const DataModelDetails = ({ ), }, + { + label: ( + + ), + key: EntityTabs.CUSTOM_PROPERTIES, + children: ( +
+ + entityDetails={dataModelData} + entityType={EntityType.DASHBOARD_DATA_MODEL} + handleExtensionUpdate={handelExtensionUpdate} + hasEditAccess={dataModelPermissions.ViewAll} + hasPermission={ + dataModelPermissions.EditAll || + dataModelPermissions.EditCustomFields + } + isVersionView={false} + /> +
+ ), + }, ]; return allTabs; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.interface.ts index 32ce111938c..dfe7352d3e1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/CustomPropertyTable/CustomPropertyTable.interface.ts @@ -14,6 +14,7 @@ import { EntityType } from '../../../enums/entity.enum'; import { Container } from '../../../generated/entity/data/container'; import { Dashboard } from '../../../generated/entity/data/dashboard'; +import { DashboardDataModel } from '../../../generated/entity/data/dashboardDataModel'; import { Database } from '../../../generated/entity/data/database'; import { DatabaseSchema } from '../../../generated/entity/data/databaseSchema'; import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; @@ -38,6 +39,7 @@ export type ExtentionEntities = { [EntityType.GLOSSARY_TERM]: GlossaryTerm; [EntityType.DATABASE]: Database; [EntityType.DATABASE_SCHEMA]: DatabaseSchema; + [EntityType.DASHBOARD_DATA_MODEL]: DashboardDataModel; }; export type ExtentionEntitiesKeys = keyof ExtentionEntities; diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/GlobalSettings.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/GlobalSettings.constants.ts index 8d124d4d4ab..10330a7aa75 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/GlobalSettings.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/GlobalSettings.constants.ts @@ -69,6 +69,7 @@ export enum GlobalSettingOptions { OM_HEALTH = 'om-health', PROFILER_CONFIGURATION = 'profiler-configuration', APPEARANCE = 'appearance', + DASHBOARD_DATA_MODEL = 'dashboardDataModels', } export const GLOBAL_SETTING_PERMISSION_RESOURCES = [ diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/PageHeaders.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/PageHeaders.constant.ts index 175e1f64b6b..c83fc2ce260 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/PageHeaders.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/PageHeaders.constant.ts @@ -108,6 +108,12 @@ export const PAGE_HEADERS = { entity: i18n.t('label.dashboard-plural'), }), }, + DASHBOARD_DATA_MODEL_CUSTOM_ATTRIBUTES: { + header: i18n.t('label.dashboard-data-model-plural'), + subHeader: i18n.t('message.define-custom-property-for-entity', { + entity: i18n.t('label.dashboard-data-model-plural'), + }), + }, PIPELINES_CUSTOM_ATTRIBUTES: { header: i18n.t('label.pipeline-plural'), subHeader: i18n.t('message.define-custom-property-for-entity', { diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts index fa4dac10ab1..ba6118d3d41 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts @@ -522,6 +522,7 @@ export const ENTITY_PATH = { glossaryTerm: 'glossaryTerm', databases: 'database', databaseSchemas: 'databaseSchema', + dashboardDataModels: 'dashboardDataModel', }; export const VALIDATION_MESSAGES = { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/CustomPropertiesPageV1/CustomPropertiesPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/CustomPropertiesPageV1/CustomPropertiesPageV1.tsx index d244e3d2dbf..25ef3552cb7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/CustomPropertiesPageV1/CustomPropertiesPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/CustomPropertiesPageV1/CustomPropertiesPageV1.tsx @@ -154,6 +154,9 @@ const CustomEntityDetailV1 = () => { case ENTITY_PATH.dashboards: return PAGE_HEADERS.DASHBOARD_CUSTOM_ATTRIBUTES; + case ENTITY_PATH.dashboardDataModels: + return PAGE_HEADERS.DASHBOARD_DATA_MODEL_CUSTOM_ATTRIBUTES; + case ENTITY_PATH.pipelines: return PAGE_HEADERS.PIPELINES_CUSTOM_ATTRIBUTES; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx index 3bd22b88264..b05197afda6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DataModelPage/DataModelPage.component.tsx @@ -34,6 +34,7 @@ import { ResourceEntity, } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; +import { TabSpecificField } from '../../enums/entity.enum'; import { CreateThread } from '../../generated/api/feed/createThread'; import { Tag } from '../../generated/entity/classification/tag'; import { DashboardDataModel } from '../../generated/entity/data/dashboardDataModel'; @@ -124,7 +125,8 @@ const DataModelsPage = () => { setIsLoading(true); try { const response = await getDataModelByFqn(dashboardDataModelFQN, { - fields: 'owner,tags,followers,votes,domain,dataProducts', + // eslint-disable-next-line max-len + fields: `${TabSpecificField.OWNER},${TabSpecificField.TAGS},${TabSpecificField.FOLLOWERS},${TabSpecificField.VOTES},${TabSpecificField.DOMAIN},${TabSpecificField.DATA_PRODUCTS},${TabSpecificField.EXTENSION}`, include: Include.All, }); setDataModelData(response); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx index 65dcb148e35..1862c513b9a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/EntityVersionPage/EntityVersionPage.component.tsx @@ -593,6 +593,7 @@ const EntityVersionPage: FunctionComponent = () => { dataProducts={currentVersionData.dataProducts} deleted={currentVersionData.deleted} domain={domain} + entityPermissions={entityPermissions} isVersionLoading={isVersionLoading} owner={owner} slashedDataModelName={slashedEntityName} diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx index f0902ffbe5a..f2e1888e61f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx @@ -18,6 +18,7 @@ import { ReactComponent as BotIcon } from '../assets/svg/bot-colored.svg'; import { ReactComponent as AppearanceIcon } from '../assets/svg/custom-logo-colored.svg'; import { ReactComponent as CustomDashboardLogoIcon } from '../assets/svg/customize-landing-page-colored.svg'; import { ReactComponent as DashboardIcon } from '../assets/svg/dashboard-colored.svg'; +import { ReactComponent as DashboardDataModelIcon } from '../assets/svg/data-model.svg'; import { ReactComponent as DatabaseIcon } from '../assets/svg/database-colored.svg'; import { ReactComponent as SchemaIcon } from '../assets/svg/database-schema.svg'; import { ReactComponent as EmailIcon } from '../assets/svg/email-colored.svg'; @@ -354,6 +355,15 @@ export const getGlobalSettingsMenuWithPermission = ( key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.DASHBOARDS}`, icon: DashboardIcon, }, + { + label: i18next.t('label.dashboard-data-model-plural'), + description: i18next.t('message.define-custom-property-for-entity', { + entity: i18next.t('label.dashboard-data-model-plural'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.DASHBOARD_DATA_MODEL}`, + icon: DashboardDataModelIcon, + }, { label: i18next.t('label.pipeline-plural'), description: i18next.t('message.define-custom-property-for-entity', {