Feat #10584 : Support Activity tab for DataModel Entity (#10941)

* Supported DataModel in Dashboard Page

* url fqn changes

* Support Activity tab for DataModel Entity

* Remove dataModel resource descriptor

* Fix tasks for dashboard data models

* changes as per comments

* minor fix

* changes as per comments and minor improvements

---------

Co-authored-by: Nahuel Verdugo Revigliono <nahuel@getcollate.io>
This commit is contained in:
Ashish Gupta 2023-04-07 11:35:40 +05:30 committed by GitHub
parent 27984c25f3
commit c997d8c80b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 325 additions and 27 deletions

View File

@ -19,6 +19,7 @@ import static org.openmetadata.schema.type.Relationship.CREATED;
import static org.openmetadata.schema.type.Relationship.IS_ABOUT;
import static org.openmetadata.schema.type.Relationship.REPLIED_TO;
import static org.openmetadata.service.Entity.DASHBOARD;
import static org.openmetadata.service.Entity.DASHBOARD_DATA_MODEL;
import static org.openmetadata.service.Entity.DATABASE_SCHEMA;
import static org.openmetadata.service.Entity.FIELD_DESCRIPTION;
import static org.openmetadata.service.Entity.PIPELINE;
@ -61,6 +62,7 @@ import org.openmetadata.schema.api.feed.EntityLinkThreadCount;
import org.openmetadata.schema.api.feed.ResolveTask;
import org.openmetadata.schema.api.feed.ThreadCount;
import org.openmetadata.schema.entity.data.Dashboard;
import org.openmetadata.schema.entity.data.DashboardDataModel;
import org.openmetadata.schema.entity.data.DatabaseSchema;
import org.openmetadata.schema.entity.data.Pipeline;
import org.openmetadata.schema.entity.data.Table;
@ -312,6 +314,50 @@ public class FeedRepository {
patch = JsonUtils.getJsonPatch(oldJson, updatedEntityJson);
repository.patch(uriInfo, dashboard.getId(), user, patch);
break;
case DASHBOARD_DATA_MODEL:
DashboardDataModel dashboardDataModel = JsonUtils.readValue(json, DashboardDataModel.class);
oldJson = JsonUtils.pojoToJson(dashboardDataModel);
if (entityLink.getFieldName() != null) {
if (entityLink.getFieldName().equals("columns")) {
Optional<Column> col =
dashboardDataModel.getColumns().stream()
.filter(c -> c.getName().equals(entityLink.getArrayFieldName()))
.findFirst();
if (col.isPresent()) {
Column column = col.get();
if (descriptionTasks.contains(taskType)) {
column.setDescription(newValue);
} else if (tagTasks.contains(taskType)) {
List<TagLabel> tags = JsonUtils.readObjects(newValue, TagLabel.class);
column.setTags(tags);
}
} else {
throw new IllegalArgumentException(
String.format(
"The Column with name '%s' is not found in the dashboard data model.",
entityLink.getArrayFieldName()));
}
} else if (descriptionTasks.contains(taskType) && entityLink.getFieldName().equals(FIELD_DESCRIPTION)) {
dashboardDataModel.setDescription(newValue);
} else if (tagTasks.contains(taskType) && entityLink.getFieldName().equals("tags")) {
List<TagLabel> tags = JsonUtils.readObjects(newValue, TagLabel.class);
dashboardDataModel.setTags(tags);
} else {
// Not supported
throw new IllegalArgumentException(
String.format(UNSUPPORTED_FIELD_NAME_FOR_TASK, entityLink.getFieldName(), task.getType()));
}
} else {
// Not supported
throw new IllegalArgumentException(
String.format(
"The Entity link with no field name - %s is not supported for %s task.",
entityLink, task.getType()));
}
updatedEntityJson = JsonUtils.pojoToJson(dashboardDataModel);
patch = JsonUtils.getJsonPatch(oldJson, updatedEntityJson);
repository.patch(uriInfo, dashboardDataModel.getId(), user, patch);
break;
case PIPELINE:
Pipeline pipeline = JsonUtils.readValue(json, Pipeline.class);
oldJson = JsonUtils.pojoToJson(pipeline);

View File

@ -549,20 +549,6 @@
"EditDescription"
]
},
{
"name": "dataModel",
"operations": [
"Create",
"Delete",
"ViewAll",
"EditAll",
"EditDescription",
"EditDisplayName",
"EditTags",
"EditOwner",
"EditLineage"
]
},
{
"name": "dashboardDataModel",
"operations": [

View File

@ -33,6 +33,7 @@ ENTITY_TYPE
| 'testDefinition'
| 'testSuite'
| 'testCase'
| 'dashboardDataModel'
;
ENTITY_FIELD
: 'columns'

View File

@ -51,6 +51,7 @@ export enum AssetsType {
DASHBOARD = 'dashboard',
PIPELINE = 'pipeline',
MLMODEL = 'mlmodel',
DASHBOARD_DATA_MODEL = 'dashboardDataModel',
}
export enum ChangeType {

View File

@ -298,6 +298,7 @@
"feature-plural-used": "Features Used",
"february": "February",
"feed-lowercase": "feed",
"feed-plural": "Feeds",
"field-change": "Field Change",
"field-invalid": "{{field}} is invalid",
"field-plural": "Fields",

View File

@ -298,6 +298,7 @@
"feature-plural-used": "Funcionalidades utilizadas",
"february": "Febrero",
"feed-lowercase": "feed",
"feed-plural": "Feeds",
"field-change": "Cambio de Campo",
"field-invalid": "{{field}} es inválido",
"field-plural": "Campos",

View File

@ -298,6 +298,7 @@
"feature-plural-used": "Features Used",
"february": "February",
"feed-lowercase": "feed",
"feed-plural": "Feeds",
"field-change": "Field Change",
"field-invalid": "{{field}} est imvalide",
"field-plural": "Champs",

View File

@ -298,6 +298,7 @@
"feature-plural-used": "使用される機能",
"february": "2月",
"feed-lowercase": "フィード",
"feed-plural": "Feeds",
"field-change": "フィールドを変更",
"field-invalid": "{{field}}は不正です",
"field-plural": "フィールド",

View File

@ -298,6 +298,7 @@
"feature-plural-used": "Funções usadas",
"february": "Fevereiro",
"feed-lowercase": "feed",
"feed-plural": "Feeds",
"field-change": "Mudança de campo",
"field-invalid": "{{field}} inválido",
"field-plural": "Campos",

View File

@ -298,6 +298,7 @@
"feature-plural-used": "Features Used",
"february": "February",
"feed-lowercase": "feed",
"feed-plural": "Feeds",
"field-change": "域变动",
"field-invalid": "{{field}} 无效",
"field-plural": "域",

View File

@ -11,9 +11,11 @@
* limitations under the License.
*/
import { Card, Space, Tabs } from 'antd';
import { Card, Col, Row, Space, Tabs } from 'antd';
import AppState from 'AppState';
import { AxiosError } from 'axios';
import ActivityFeedList from 'components/ActivityFeed/ActivityFeedList/ActivityFeedList';
import ActivityThreadPanel from 'components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
import Description from 'components/common/description/Description';
import EntityPageInfo from 'components/common/entityPageInfo/EntityPageInfo';
import ErrorPlaceHolder from 'components/common/error-with-placeholder/ErrorPlaceHolder';
@ -29,16 +31,22 @@ import {
import SchemaEditor from 'components/schema-editor/SchemaEditor';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { getServiceDetailsPath } from 'constants/constants';
import { ENTITY_CARD_CLASS } from 'constants/entity.constants';
import { EntityField } from 'constants/Feeds.constants';
import { NO_PERMISSION_TO_VIEW } from 'constants/HelperTextUtil';
import { CSMode } from 'enums/codemirror.enum';
import { EntityInfo, EntityType } from 'enums/entity.enum';
import { FeedFilter } from 'enums/mydata.enum';
import { ServiceCategory } from 'enums/service.enum';
import { OwnerType } from 'enums/user.enum';
import { compare } from 'fast-json-patch';
import { compare, Operation } from 'fast-json-patch';
import { CreateThread } from 'generated/api/feed/createThread';
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
import { Post, Thread, ThreadType } from 'generated/entity/feed/thread';
import { Paging } from 'generated/type/paging';
import { LabelType, State, TagSource } from 'generated/type/tagLabel';
import { isUndefined, omitBy } from 'lodash';
import { EntityFieldThreadCount } from 'interface/feed.interface';
import jsonData from 'jsons/en';
import { isUndefined, omitBy, toString } from 'lodash';
import { EntityTags, ExtraInfo } from 'Models';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -49,14 +57,22 @@ import {
patchDataModelDetails,
removeDataModelFollower,
} from 'rest/dataModelsAPI';
import { getAllFeeds, postFeedById, postThread } from 'rest/feedsAPI';
import {
getCountBadge,
getCurrentUserId,
getEntityMissingError,
getEntityPlaceHolder,
getFeedCounts,
getOwnerValue,
} from 'utils/CommonUtils';
import { getDataModelsDetailPath } from 'utils/DataModelsUtils';
import { getEntityName } from 'utils/EntityUtils';
import { getEntityFeedLink, getEntityName } from 'utils/EntityUtils';
import {
deletePost,
getEntityFieldThreadCounts,
updateThreadData,
} from 'utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from 'utils/PermissionsUtils';
import { serviceTypeLogo } from 'utils/ServiceUtils';
import { getTagsWithoutTier, getTierTags } from 'utils/TableUtils';
@ -77,6 +93,21 @@ const DataModelsPage = () => {
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
const [dataModelData, setDataModelData] = useState<DashboardDataModel>();
const [threadLink, setThreadLink] = useState<string>('');
const [entityThread, setEntityThread] = useState<Thread[]>([]);
const [isEntityThreadLoading, setIsEntityThreadLoading] =
useState<boolean>(false);
const [paging, setPaging] = useState<Paging>({} as Paging);
const [feedCount, setFeedCount] = useState<number>(0);
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
EntityFieldThreadCount[]
>([]);
const [entityFieldTaskCount, setEntityFieldTaskCount] = useState<
EntityFieldThreadCount[]
>([]);
// get current user details
const currentUser = useMemo(
() => AppState.getCurrentUserDetails(),
@ -159,6 +190,110 @@ const DataModelsPage = () => {
];
}, [dataModelData, dashboardDataModelFQN, entityName]);
const getFeedData = useCallback(
async (
after?: string,
feedFilter?: FeedFilter,
threadType?: ThreadType
) => {
setIsEntityThreadLoading(true);
!after && setEntityThread([]);
try {
const { data, paging: pagingObj } = await getAllFeeds(
getEntityFeedLink(
EntityType.DASHBOARD_DATA_MODEL,
dashboardDataModelFQN
),
after,
threadType,
feedFilter,
undefined,
currentUser?.id
);
setPaging(pagingObj);
setEntityThread((prevData) => [...prevData, ...data]);
} catch (err) {
showErrorToast(
err as AxiosError,
t('server.entity-fetch-error', {
entity: t('label.feed-plural'),
})
);
} finally {
setIsEntityThreadLoading(false);
}
},
[dashboardDataModelFQN]
);
const getEntityFeedCount = () => {
getFeedCounts(
EntityType.DASHBOARD_DATA_MODEL,
dashboardDataModelFQN,
setEntityFieldThreadCount,
setEntityFieldTaskCount,
setFeedCount
);
};
const deletePostHandler = (
threadId: string,
postId: string,
isThread: boolean
) => {
deletePost(threadId, postId, isThread, setEntityThread);
};
const onThreadLinkSelect = (link: string) => {
setThreadLink(link);
};
const postFeedHandler = (value: string, id: string) => {
const currentUser = AppState.userDetails?.name ?? AppState.users[0]?.name;
const data = {
message: value,
from: currentUser,
} as Post;
postFeedById(id, data)
.then((res) => {
if (res) {
const { id, posts } = res;
setEntityThread((pre) => {
return pre.map((thread) => {
if (thread.id === id) {
return { ...res, posts: posts?.slice(-3) };
} else {
return thread;
}
});
});
getEntityFeedCount();
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(err, jsonData['api-error-messages']['add-feed-error']);
});
};
const updateThreadHandler = (
threadId: string,
postId: string,
isThread: boolean,
data: Operation[]
) => {
updateThreadData(threadId, postId, isThread, data, setEntityThread);
};
const handleFeedFilterChange = useCallback(
(feedType, threadType) => {
getFeedData(undefined, feedType, threadType);
},
[paging]
);
const fetchResourcePermission = async (dashboardDataModelFQN: string) => {
setIsLoading(true);
try {
@ -178,6 +313,25 @@ const DataModelsPage = () => {
}
};
const createThread = (data: CreateThread) => {
postThread(data)
.then((res) => {
if (res) {
setEntityThread((pre) => [...pre, res]);
getEntityFeedCount();
} else {
showErrorToast(
jsonData['api-error-messages']['unexpected-server-response']
);
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['create-conversation-error']
);
});
};
const fetchDataModelDetails = async (dashboardDataModelFQN: string) => {
setIsLoading(true);
try {
@ -194,10 +348,8 @@ const DataModelsPage = () => {
}
};
const handleUpdateDataModelData = (updatedData: DashboardDataModel) => {
const jsonPatch = compare(omitBy(dataModelData, isUndefined), updatedData);
return patchDataModelDetails(dataModelData?.id ?? '', jsonPatch);
const onThreadPanelClose = () => {
setThreadLink('');
};
const handleTabChange = (tabValue: string) => {
@ -208,6 +360,12 @@ const DataModelsPage = () => {
}
};
const handleUpdateDataModelData = (updatedData: DashboardDataModel) => {
const jsonPatch = compare(omitBy(dataModelData, isUndefined), updatedData);
return patchDataModelDetails(dataModelData?.id ?? '', jsonPatch);
};
const handleUpdateDescription = async (updatedDescription: string) => {
try {
const { description: newDescription, version } =
@ -301,6 +459,7 @@ const DataModelsPage = () => {
tags: newTags,
version,
}));
getEntityFeedCount();
} catch (error) {
showErrorToast(error as AxiosError);
}
@ -319,6 +478,7 @@ const DataModelsPage = () => {
owner: newOwner,
version,
}));
getEntityFeedCount();
} catch (error) {
showErrorToast(error as AxiosError);
}
@ -347,6 +507,7 @@ const DataModelsPage = () => {
tags: newTags,
version,
}));
getEntityFeedCount();
}
} catch (error) {
showErrorToast(error as AxiosError);
@ -372,9 +533,16 @@ const DataModelsPage = () => {
}
};
useEffect(() => {
if (tab === DATA_MODELS_DETAILS_TABS.ACTIVITY) {
getFeedData();
}
}, [tab, dashboardDataModelFQN]);
useEffect(() => {
if (hasViewPermission) {
fetchDataModelDetails(dashboardDataModelFQN);
getEntityFeedCount();
}
}, [dashboardDataModelFQN, dataModelPermissions]);
@ -406,6 +574,14 @@ const DataModelsPage = () => {
canDelete={dataModelPermissions.Delete}
currentOwner={owner}
deleted={deleted}
entityFieldTasks={getEntityFieldThreadCounts(
EntityField.TAGS,
entityFieldTaskCount
)}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.TAGS,
entityFieldThreadCount
)}
entityFqn={dashboardDataModelFQN}
entityId={entityId}
entityName={entityName || ''}
@ -423,7 +599,8 @@ const DataModelsPage = () => {
titleLinks={breadcrumbTitles}
updateOwner={hasEditOwnerPermission ? handleUpdateOwner : undefined}
updateTier={hasEditTierPermission ? handleUpdateTier : undefined}
version={version + ''}
version={toString(version)}
onThreadLinkSelect={onThreadLinkSelect}
/>
<Tabs activeKey={tab} className="h-full" onChange={handleTabChange}>
<Tabs.TabPane
@ -433,7 +610,7 @@ const DataModelsPage = () => {
{t('label.model')}
</span>
}>
<Card className={ENTITY_CARD_CLASS}>
<Card className="h-full">
<Space className="w-full" direction="vertical" size={8}>
<Description
description={description}
@ -460,6 +637,38 @@ const DataModelsPage = () => {
</Card>
</Tabs.TabPane>
<Tabs.TabPane
key={DATA_MODELS_DETAILS_TABS.ACTIVITY}
tab={
<span data-testid={DATA_MODELS_DETAILS_TABS.ACTIVITY}>
{t('label.activity-feed-and-task-plural')}{' '}
{getCountBadge(
feedCount,
'',
DATA_MODELS_DETAILS_TABS.ACTIVITY === tab
)}
</span>
}>
<Card className="m-y-md">
<Row justify="center">
<Col span={18}>
<div id="activityfeed">
<ActivityFeedList
isEntityFeed
withSidePanel
deletePostHandler={deletePostHandler}
entityName={entityName}
feedList={entityThread}
isFeedLoading={isEntityThreadLoading}
postFeedHandler={postFeedHandler}
updateThreadHandler={updateThreadHandler}
onFeedFiltersUpdate={handleFeedFilterChange}
/>
</div>
</Col>
</Row>
</Card>
</Tabs.TabPane>
{dataModelData?.sql && (
<Tabs.TabPane
key={DATA_MODELS_DETAILS_TABS.SQL}
@ -468,7 +677,7 @@ const DataModelsPage = () => {
{t('label.sql-uppercase')}
</span>
}>
<Card className={ENTITY_CARD_CLASS}>
<Card className="h-full">
<SchemaEditor
editorClass="custom-code-mirror-theme full-screen-editor-height"
mode={{ name: CSMode.SQL }}
@ -500,6 +709,18 @@ const DataModelsPage = () => {
</Card>
</Tabs.TabPane>
</Tabs>
{threadLink ? (
<ActivityThreadPanel
createThread={createThread}
deletePostHandler={deletePostHandler}
open={Boolean(threadLink)}
postFeedHandler={postFeedHandler}
threadLink={threadLink}
updateThreadHandler={updateThreadHandler}
onCancel={onThreadPanelClose}
/>
) : null}
</div>
</PageContainerV1>
);

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
import { DatabaseSchema } from 'generated/entity/data/databaseSchema';
import { Dashboard } from '../../generated/entity/data/dashboard';
import { Mlmodel } from '../../generated/entity/data/mlmodel';
@ -24,7 +25,8 @@ export type EntityData =
| Dashboard
| Pipeline
| Mlmodel
| DatabaseSchema;
| DatabaseSchema
| DashboardDataModel;
export interface Option {
label: string;

View File

@ -14,6 +14,7 @@ import { AxiosResponse } from 'axios';
import { Operation } from 'fast-json-patch';
import { DashboardDataModel } from 'generated/entity/data/dashboardDataModel';
import { EntityReference } from 'generated/type/entityReference';
import { getURLWithQueryFields } from 'utils/APIUtils';
import APIClient from './index';
const URL = '/dashboard/datamodels';
@ -37,6 +38,20 @@ export const getDataModelsByName = async (
return response.data;
};
export const getDataModelDetailsByFQN = async (
databaseSchemaName: string,
arrQueryFields?: string | string[]
) => {
const url = `${getURLWithQueryFields(
`/dashboard/datamodels/name/${databaseSchemaName}`,
arrQueryFields
)}`;
const response = await APIClient.get<DashboardDataModel>(url);
return response.data;
};
export const patchDataModelDetails = async (id: string, data: Operation[]) => {
const response = await APIClient.patch<
Operation[],

View File

@ -15,6 +15,7 @@ import {
PLACEHOLDER_ROUTE_TAB,
ROUTES,
} from 'constants/constants';
import { TabSpecificField } from 'enums/entity.enum';
import { Column } from 'generated/entity/data/dashboardDataModel';
import { LabelType, State, TagLabel } from 'generated/type/tagLabel';
import { isEmpty } from 'lodash';
@ -112,3 +113,6 @@ export const updateDataModelColumnTags = (
}
});
};
export const defaultFields = `${TabSpecificField.TAGS}, ${TabSpecificField.OWNER},
${TabSpecificField.FOLLOWERS}`;

View File

@ -43,6 +43,7 @@ import {
getDashboardDetailsPath,
getDatabaseDetailsPath,
getDatabaseSchemaDetailsPath,
getDataModelDetailsPath,
getEditWebhookPath,
getMlModelPath,
getPipelineDetailsPath,
@ -260,6 +261,9 @@ export const getEntityLink = (
case SearchIndex.TAG:
return getTagsDetailsPath(fullyQualifiedName);
case EntityType.DASHBOARD_DATA_MODEL:
return getDataModelDetailsPath(fullyQualifiedName);
case SearchIndex.TABLE:
case EntityType.TABLE:
default:

View File

@ -23,6 +23,7 @@ import {
} from 'pages/TasksPage/TasksPage.interface';
import { getDashboardByFqn } from 'rest/dashboardAPI';
import { getDatabaseSchemaDetailsByFQN } from 'rest/databaseAPI';
import { getDataModelDetailsByFQN } from 'rest/dataModelsAPI';
import { getUserSuggestions } from 'rest/miscAPI';
import { getMlModelByFQN } from 'rest/mlModelAPI';
import { getPipelineByFqn } from 'rest/pipelineAPI';
@ -44,6 +45,7 @@ import { TaskType } from '../generated/entity/feed/thread';
import { getPartialNameFromTableFQN } from './CommonUtils';
import { defaultFields as DashboardFields } from './DashboardDetailsUtils';
import { defaultFields as DatabaseSchemaFields } from './DatabaseSchemaDetailsUtils';
import { defaultFields as DataModelFields } from './DataModelsUtils';
import { defaultFields as TableFields } from './DatasetDetailsUtils';
import { getEntityName } from './EntityUtils';
import { defaultFields as MlModelFields } from './MlModelDetailsUtils';
@ -189,6 +191,7 @@ export const TASK_ENTITIES = [
EntityType.PIPELINE,
EntityType.MLMODEL,
EntityType.DATABASE_SCHEMA,
EntityType.DASHBOARD_DATA_MODEL,
];
export const getBreadCrumbList = (
@ -327,6 +330,15 @@ export const fetchEntityDetail = (
break;
case EntityType.DASHBOARD_DATA_MODEL:
getDataModelDetailsByFQN(entityFQN, DataModelFields)
.then((res) => {
setEntityData(res);
})
.catch((err: AxiosError) => showErrorToast(err));
break;
default:
break;
}