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 <sonikashah94@gmail.com>
This commit is contained in:
Sachin Chaurasiya 2024-06-13 20:51:47 +05:30 committed by GitHub
parent fc9033b953
commit 55cd180ffa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 135 additions and 31 deletions

View File

@ -71,7 +71,7 @@ import org.openmetadata.service.util.ResultList;
public class DashboardDataModelResource
extends EntityResource<DashboardDataModel, DashboardDataModelRepository> {
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) {

View File

@ -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"],

View File

@ -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": [

View File

@ -25,6 +25,7 @@ export const CustomPropertySupportedEntityList = [
EntityTypeEndpoint.MlModel,
EntityTypeEndpoint.GlossaryTerm,
EntityTypeEndpoint.SearchIndex,
EntityTypeEndpoint.DataModel,
];
export const ENTITY_REFERENCE_PROPERTIES = [

View File

@ -56,4 +56,5 @@ export enum ENTITY_PATH {
glossaryTerm = 'glossaryTerm',
databases = 'database',
databaseSchemas = 'databaseSchema',
'dashboard/datamodels' = 'dashboardDataModel',
}

View File

@ -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: {

View File

@ -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<DataModelVersionProp> = ({
deleted = false,
backHandler,
versionHandler,
entityPermissions,
}: DataModelVersionProp) => {
const history = useHistory();
const { t } = useTranslation();
const { tab } = useParams<{ tab: EntityTabs }>();
const [changeDescription, setChangeDescription] = useState<ChangeDescription>(
currentVersionData.changeDescription as ChangeDescription
);
const entityFqn = useMemo(
() => currentVersionData.fullyQualifiedName ?? '',
[currentVersionData.fullyQualifiedName]
);
const { ownerDisplayName, ownerRef, tierDisplayName, domainDisplayName } =
useMemo(
() =>
@ -107,6 +118,17 @@ const DataModelVersion: FC<DataModelVersionProp> = ({
);
}, [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<DataModelVersionProp> = ({
</Row>
),
},
{
key: EntityTabs.CUSTOM_PROPERTIES,
label: (
<TabsLabel
id={EntityTabs.CUSTOM_PROPERTIES}
name={t('label.custom-property-plural')}
/>
),
children: (
<div className="p-md">
<CustomPropertyTable
isVersionView
entityDetails={currentVersionData as DashboardDataModel}
entityType={EntityType.DASHBOARD_DATA_MODEL}
hasEditAccess={false}
hasPermission={entityPermissions.ViewAll}
/>
</div>
),
},
],
[description, columns]
[description, columns, currentVersionData, entityPermissions]
);
return (
@ -191,7 +233,11 @@ const DataModelVersion: FC<DataModelVersionProp> = ({
/>
</Col>
<Col span={24}>
<Tabs activeKey={EntityTabs.MODEL} items={tabItems} />
<Tabs
activeKey={tab ?? EntityTabs.MODEL}
items={tabItems}
onChange={handleTabChange}
/>
</Col>
</Row>
</div>

View File

@ -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;
}

View File

@ -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: (
<div data-testid="entity-right-panel">
<EntityRightPanel
<EntityRightPanel<EntityType.DASHBOARD_DATA_MODEL>
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 = ({
</LineageProvider>
),
},
{
label: (
<TabsLabel
id={EntityTabs.CUSTOM_PROPERTIES}
name={t('label.custom-property-plural')}
/>
),
key: EntityTabs.CUSTOM_PROPERTIES,
children: (
<div className="p-md">
<CustomPropertyTable<EntityType.DASHBOARD_DATA_MODEL>
entityDetails={dataModelData}
entityType={EntityType.DASHBOARD_DATA_MODEL}
handleExtensionUpdate={handelExtensionUpdate}
hasEditAccess={dataModelPermissions.ViewAll}
hasPermission={
dataModelPermissions.EditAll ||
dataModelPermissions.EditCustomFields
}
isVersionView={false}
/>
</div>
),
},
];
return allTabs;

View File

@ -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;

View File

@ -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 = [

View File

@ -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', {

View File

@ -522,6 +522,7 @@ export const ENTITY_PATH = {
glossaryTerm: 'glossaryTerm',
databases: 'database',
databaseSchemas: 'databaseSchema',
dashboardDataModels: 'dashboardDataModel',
};
export const VALIDATION_MESSAGES = {

View File

@ -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;

View File

@ -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);

View File

@ -593,6 +593,7 @@ const EntityVersionPage: FunctionComponent = () => {
dataProducts={currentVersionData.dataProducts}
deleted={currentVersionData.deleted}
domain={domain}
entityPermissions={entityPermissions}
isVersionLoading={isVersionLoading}
owner={owner}
slashedDataModelName={slashedEntityName}

View File

@ -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', {