diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/img/feedEditor.png b/openmetadata-ui/src/main/resources/ui/src/assets/img/feedEditor.png new file mode 100644 index 00000000000..66db5801ecb Binary files /dev/null and b/openmetadata-ui/src/main/resources/ui/src/assets/img/feedEditor.png differ diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/add-chat.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/add-chat.svg new file mode 100644 index 00000000000..c1e4505a68c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/add-chat.svg @@ -0,0 +1,3 @@ + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/comment.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/comment.svg new file mode 100644 index 00000000000..2a8ba9b742b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/comment.svg @@ -0,0 +1,4 @@ + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/paper-plane-primary.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/paper-plane-primary.svg new file mode 100644 index 00000000000..3894ec6718b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/paper-plane-primary.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/paper-plane.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/paper-plane.svg index 679e15590c9..0fa829bc13c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/paper-plane.svg +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/paper-plane.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/request-icon.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/request-icon.svg new file mode 100644 index 00000000000..a216d951fcf --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/request-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/feedsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/feedsAPI.ts index ee8ed3be3af..04e68ef08af 100644 --- a/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/feedsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/axiosAPIs/feedsAPI.ts @@ -12,11 +12,11 @@ */ import { AxiosResponse } from 'axios'; +import { isUndefined } from 'lodash'; import { Post } from 'Models'; +import { FeedFilter } from '../enums/mydata.enum'; import { CreateThread } from '../generated/api/feed/createThread'; import APIClient from './index'; -import { FeedFilter } from '../enums/mydata.enum'; -import { isUndefined } from 'lodash'; export const getAllFeeds: Function = ( entityLink?: string diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx index a8ea49f4f93..03ac74a757b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx @@ -14,7 +14,7 @@ import classNames from 'classnames'; import React, { FC, HTMLAttributes, useRef, useState } from 'react'; import { getBackendFormat, HTMLToMarkdown } from '../../../utils/FeedUtils'; -import SVGIcons from '../../../utils/SvgUtils'; +import SVGIcons, { Icons } from '../../../utils/SvgUtils'; import { Button } from '../../buttons/Button/Button'; import PopOver from '../../common/popover/PopOver'; import FeedEditor from '../../FeedEditor/FeedEditor'; @@ -23,6 +23,7 @@ interface ActivityFeedEditorProp extends HTMLAttributes { onSave?: (value: string) => void; buttonClass?: string; placeHolder?: string; + defaultValue?: string; } type EditorContentRef = { getEditorValue: () => string; @@ -34,6 +35,7 @@ const ActivityFeedEditor: FC = ({ buttonClass = '', onSave, placeHolder, + defaultValue, }) => { const editorRef = useRef(); const [editorValue, setEditorValue] = useState(''); @@ -54,6 +56,7 @@ const ActivityFeedEditor: FC = ({ return (
= ({ trigger="mouseenter">
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedList.tsx index fb3fa6e3721..77532fd469e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedList/ActivityFeedList.tsx @@ -28,11 +28,13 @@ import ActivityFeedCard, { } from '../ActivityFeedCard/ActivityFeedCard'; import ActivityFeedEditor from '../ActivityFeedEditor/ActivityFeedEditor'; import ActivityFeedPanel from '../ActivityFeedPanel/ActivityFeedPanel'; +import NoFeedPlaceholder from '../NoFeedPlaceholder/NoFeedPlaceholder'; interface ActivityFeedListProp extends HTMLAttributes { feedList: EntityThread[]; withSidePanel?: boolean; isEntityFeed?: boolean; + entityName?: string; postFeedHandler?: (value: string, id: string) => void; } interface FeedListSeparatorProp extends HTMLAttributes { @@ -61,7 +63,7 @@ export const FeedListSeparator: FC = ({

{relativeDay ? ( - + {relativeDay} ) : null} @@ -91,8 +93,8 @@ const FeedListBody: FC = ({ from: feed.createdBy, }; const postLength = feed.posts.length; - const replies = feed.postsCount; - const repliedUsers = feed.posts.map((f) => f.from); + const replies = feed.postsCount - 1; + const repliedUsers = feed.posts.map((f) => f.from).slice(1, 3); const lastPost = feed.posts[postLength - 1]; return ( @@ -105,15 +107,11 @@ const FeedListBody: FC = ({ /> {postLength > 0 ? ( -
= ({ onViewMore(); }} /> - - | - -

onThreadIdSelect(feed.id)}> - Reply -

- {selctedThreadId === feed.id ? ( - - ) : null}
+ +

{ + onThreadIdSelect(feed.id); + }}> + Reply +

+ {selctedThreadId === feed.id ? ( + + ) : null}
) : (

= ({ withSidePanel = false, isEntityFeed = false, postFeedHandler, + entityName, }) => { const { updatedFeedList, relativeDays } = getFeedListWithRelativeDays(feedList); @@ -256,11 +259,17 @@ const ActivityFeedList: FC = ({ ) : ( - - <>No conversations found. Try changing the filter. + {entityName ? ( + + ) : ( + + + <>No conversations found. Try changing the filter. + + )} )}

diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/ActivityFeedPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/ActivityFeedPanel.tsx index f4d42db2fdb..53a37cbdf57 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/ActivityFeedPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedPanel/ActivityFeedPanel.tsx @@ -38,6 +38,7 @@ interface FeedPanelHeaderProp extends HTMLAttributes, Pick { entityField: string; + noun?: string; } interface FeedPanelOverlayProp extends HTMLAttributes, @@ -51,12 +52,14 @@ export const FeedPanelHeader: FC = ({ onCancel, entityField, className, + noun, }) => { return (

- Thread on {entityField} + {noun ? noun : 'Conversation'} on{' '} + {entityField}

= ({ {repliesLength > 0 ? (
- {getReplyText(repliesLength)} + {getReplyText(repliesLength, 'reply', 'replies')}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx index f4d6e5fc51e..1b668e6607f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx @@ -89,8 +89,10 @@ const ActivityThreadList: FC = ({ from: thread.createdBy, }; const postLength = thread.posts.length; - const replies = thread.postsCount; - const repliedUsers = thread.posts.map((f) => f.from); + const replies = thread.postsCount - 1; + const repliedUsers = thread.posts + .map((f) => f.from) + .slice(1, 3); const lastPost = thread.posts[postLength - 1]; return ( @@ -103,31 +105,31 @@ const ActivityThreadList: FC = ({ /> {postLength > 0 ? ( -
onThreadSelect(thread.id)} /> - - | - -

onThreadIdSelect(thread.id)}> - Reply -

+ +

{ + onThreadIdSelect(thread.id); + }}> + Reply +

) : (

= ({ {repliesLength > 0 ? (

- {getReplyText(repliesLength)} + {getReplyText(repliesLength, 'reply', 'replies')}
@@ -304,6 +306,7 @@ const ActivityThreadPanel: FC = ({ {!isUndefined(selectedThread) ? ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/NoFeedPlaceholder/NoFeedPlaceholder.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/NoFeedPlaceholder/NoFeedPlaceholder.tsx new file mode 100644 index 00000000000..da5aa6cf318 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/NoFeedPlaceholder/NoFeedPlaceholder.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2021 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 classNames from 'classnames'; +import React, { FC, HTMLAttributes } from 'react'; +import EditorImg from '../../../assets/img/feedEditor.png'; +import SVGIcons, { Icons } from '../../../utils/SvgUtils'; + +interface NoFeedPlaceholderProp extends HTMLAttributes { + entityName: string; +} + +const NoFeedPlaceholder: FC = ({ + className, + entityName, +}) => { + return ( +
+ {`There is no activity on the "${entityName}" yet. Start a conversation by clicking + on the `} + + + + {` to collaborate with other users.`} + +
+ editor-image +
+
+ ); +}; + +export default NoFeedPlaceholder; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx index 27605d25ace..7f7b00d203b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.component.tsx @@ -17,9 +17,10 @@ import { EntityTags } from 'Models'; import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { getTeamDetailsPath } from '../../constants/constants'; +import { EntityType } from '../../enums/entity.enum'; import { Dashboard } from '../../generated/entity/data/dashboard'; import { Operation } from '../../generated/entity/policies/accessControl/rule'; -import { User } from '../../generated/entity/teams/user'; +import { EntityReference, User } from '../../generated/entity/teams/user'; import { LabelType, State, TagLabel } from '../../generated/type/tagLabel'; import { useAuth } from '../../hooks/authHooks'; import { @@ -28,6 +29,8 @@ import { getUserTeams, isEven, } from '../../utils/CommonUtils'; +import { getEntityFeedLink } from '../../utils/EntityUtils'; +import { getDefaultValue } from '../../utils/FeedElementUtils'; import { getEntityFieldThreadCounts } from '../../utils/FeedUtils'; import SVGIcons from '../../utils/SvgUtils'; import { getTagsWithoutTier } from '../../utils/TableUtils'; @@ -43,6 +46,7 @@ import PageContainer from '../containers/PageContainer'; import Entitylineage from '../EntityLineage/EntityLineage.component'; import ManageTabComponent from '../ManageTab/ManageTab.component'; import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; +import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal'; import TagsContainer from '../tags-container/tags-container'; import Tags from '../tags/tags'; import { ChartType, DashboardDetailsProps } from './DashboardDetails.interface'; @@ -86,6 +90,7 @@ const DashboardDetails = ({ feedCount, entityFieldThreadCount, createThread, + dashboardFQN, }: DashboardDetailsProps) => { const { isAuthDisabled } = useAuth(); const [isEdit, setIsEdit] = useState(false); @@ -102,6 +107,14 @@ const DashboardDetails = ({ const [tagList, setTagList] = useState>([]); const [isTagLoading, setIsTagLoading] = useState(false); const [threadLink, setThreadLink] = useState(''); + const [selectedField, setSelectedField] = useState(''); + + const onEntityFieldSelect = (value: string) => { + setSelectedField(value); + }; + const closeRequestModal = () => { + setSelectedField(''); + }; const hasEditAccess = () => { if (owner?.type === 'user') { return owner.id === getCurrentUserId(); @@ -365,7 +378,9 @@ const DashboardDetails = ({ 'tags', entityFieldThreadCount )} + entityFqn={dashboardFQN} entityName={entityName} + entityType={EntityType.DASHBOARD} extraInfo={extraInfo} followHandler={followDashboard} followers={followersCount} @@ -400,7 +415,9 @@ const DashboardDetails = ({ 'description', entityFieldThreadCount )} + entityFqn={dashboardFQN} entityName={entityName} + entityType={EntityType.DASHBOARD} hasEditAccess={hasEditAccess()} isEdit={isEdit} isReadOnly={deleted} @@ -408,6 +425,7 @@ const DashboardDetails = ({ onCancel={onCancel} onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} + onEntityFieldSelect={onEntityFieldSelect} onThreadLinkSelect={onThreadLinkSelect} />
@@ -566,15 +584,6 @@ const DashboardDetails = ({
- {threadLink ? ( - - ) : null} )} {activeTab === 2 && ( @@ -586,6 +595,7 @@ const DashboardDetails = ({ isEntityFeed withSidePanel className="" + entityName={entityName} feedList={entityThread} isLoading={isentityThreadLoading} postFeedHandler={postFeedHandler} @@ -632,6 +642,28 @@ const DashboardDetails = ({ onSave={onChartUpdate} /> )} + {threadLink ? ( + + ) : null} + {selectedField ? ( + + ) : null} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts index e77c8ad75f7..70d430fe55e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.interface.ts @@ -36,6 +36,7 @@ export interface ChartType extends Chart { } export interface DashboardDetailsProps { + dashboardFQN: string; version: string; isNodeLoading: LoadingNodeState; lineageLeafNodes: LeafNodes; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.test.tsx index e2f852580dc..d1676d1e2a3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DashboardDetails/DashboardDetails.test.tsx @@ -77,6 +77,7 @@ const DashboardDetailsProps = { feedCount: 0, entityFieldThreadCount: [], createThread: jest.fn(), + dashboardFQN: '', }; jest.mock('../ManageTab/ManageTab.component', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx index 9c3ee4533bc..985767307c6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.component.tsx @@ -17,6 +17,7 @@ import { ColumnJoins, EntityTags, ExtraInfo } from 'Models'; import React, { useEffect, useState } from 'react'; import { getTeamDetailsPath, ROUTES } from '../../constants/constants'; import { CSMode } from '../../enums/codemirror.enum'; +import { EntityType } from '../../enums/entity.enum'; import { JoinedWith, Table, @@ -32,6 +33,8 @@ import { getTableFQNFromColumnFQN, getUserTeams, } from '../../utils/CommonUtils'; +import { getEntityFeedLink } from '../../utils/EntityUtils'; +import { getDefaultValue } from '../../utils/FeedElementUtils'; import { getEntityFieldThreadCounts } from '../../utils/FeedUtils'; import { getTagsWithoutTier, getUsagePercentile } from '../../utils/TableUtils'; import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList'; @@ -43,6 +46,7 @@ import PageContainer from '../containers/PageContainer'; import Entitylineage from '../EntityLineage/EntityLineage.component'; import FrequentlyJoinedTables from '../FrequentlyJoinedTables/FrequentlyJoinedTables.component'; import ManageTab from '../ManageTab/ManageTab.component'; +import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal'; import SampleDataTable, { SampleColumns, } from '../SampleDataTable/SampleDataTable.component'; @@ -111,6 +115,14 @@ const DatasetDetails: React.FC = ({ }); const [threadLink, setThreadLink] = useState(''); + const [selectedField, setSelectedField] = useState(''); + + const onEntityFieldSelect = (value: string) => { + setSelectedField(value); + }; + const closeRequestModal = () => { + setSelectedField(''); + }; const setUsageDetails = ( usageSummary: TypeUsedToReturnUsageDetailsOfAnEntity @@ -476,7 +488,9 @@ const DatasetDetails: React.FC = ({ 'description', entityFieldThreadCount )} + entityFqn={datasetFQN} entityName={entityName} + entityType={EntityType.TABLE} hasEditAccess={hasEditAccess()} isEdit={isEdit} isReadOnly={deleted} @@ -484,6 +498,7 @@ const DatasetDetails: React.FC = ({ onCancel={onCancel} onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} + onEntityFieldSelect={onEntityFieldSelect} onThreadLinkSelect={onThreadLinkSelect} />
@@ -505,11 +520,13 @@ const DatasetDetails: React.FC = ({ 'columns', entityFieldThreadCount )} + entityFqn={datasetFQN} hasEditAccess={hasEditAccess()} isReadOnly={deleted} joins={tableJoinData.columnJoins as ColumnJoins[]} owner={owner} sampleData={sampleData} + onEntityFieldSelect={onEntityFieldSelect} onThreadLinkSelect={onThreadLinkSelect} onUpdate={onColumnsUpdate} /> @@ -524,6 +541,19 @@ const DatasetDetails: React.FC = ({ onCancel={onThreadPanelClose} /> ) : null} + {selectedField ? ( + + ) : null} )} {activeTab === 2 && ( @@ -535,6 +565,7 @@ const DatasetDetails: React.FC = ({ isEntityFeed withSidePanel className="" + entityName={entityName} feedList={entityThread} isLoading={isentityThreadLoading} postFeedHandler={postFeedHandler} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.test.tsx index bdfab01542c..7e9397e3616 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DatasetDetails/DatasetDetails.test.tsx @@ -118,7 +118,10 @@ jest.mock('../ActivityFeed/ActivityFeedList/ActivityFeedList.tsx', () => { }); jest.mock('../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel.tsx', () => { - return jest.fn().mockReturnValue(

FeedCards

); + return jest.fn().mockReturnValue(

Conversations

); +}); +jest.mock('../ActivityFeed/ActivityFeedEditor/ActivityFeedEditor.tsx', () => { + return jest.fn().mockReturnValue(

FeedEditor

); }); jest.mock('../../utils/CommonUtils', () => ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx index a0b930e211a..f87e0db3570 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/EntityTable/EntityTable.component.tsx @@ -12,12 +12,13 @@ */ import classNames from 'classnames'; -import { cloneDeep, isUndefined, lowerCase } from 'lodash'; +import { cloneDeep, isNil, isUndefined, lowerCase } from 'lodash'; import { EntityFieldThreads, EntityTags } from 'Models'; import React, { Fragment, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { useExpanded, useTable } from 'react-table'; import { getTableDetailsPath } from '../../constants/constants'; +import { EntityType } from '../../enums/entity.enum'; import { Column, ColumnJoins, @@ -32,7 +33,8 @@ import { getTableFQNFromColumnFQN, } from '../../utils/CommonUtils'; import { getFieldThreadElement } from '../../utils/FeedElementUtils'; -import SVGIcons from '../../utils/SvgUtils'; +import { getThreadValue } from '../../utils/FeedUtils'; +import SVGIcons, { Icons } from '../../utils/SvgUtils'; import { getConstraintIcon, getDataTypeString, @@ -54,9 +56,11 @@ type Props = { columnName: string; hasEditAccess: boolean; isReadOnly?: boolean; + entityFqn?: string; entityFieldThreads?: EntityFieldThreads[]; onUpdate?: (columns: Table['columns']) => void; onThreadLinkSelect?: (value: string) => void; + onEntityFieldSelect?: (value: string) => void; }; const EntityTable = ({ @@ -69,6 +73,8 @@ const EntityTable = ({ entityFieldThreads, isReadOnly = false, onThreadLinkSelect, + onEntityFieldSelect, + entityFqn, }: Props) => { const columns = React.useMemo( () => [ @@ -465,7 +471,11 @@ const EntityTable = ({ cell.row.cells[0].value, 'tags', entityFieldThreads as EntityFieldThreads[], - onThreadLinkSelect + onThreadLinkSelect, + EntityType.TABLE, + entityFqn, + `columns/${cell.row.cells[0].value}/tags`, + Boolean(cell.value.length) )} )} @@ -485,39 +495,70 @@ const EntityTable = ({ /> ) : ( - No description added + No description added{' '} )} - {getFieldThreadElement( - cell.row.cells[0].value, - 'description', - entityFieldThreads as EntityFieldThreads[], - onThreadLinkSelect - )} {!isReadOnly ? ( - + + + + {isNil( + getThreadValue( + cell.row.cells[0].value, + 'description', + entityFieldThreads as EntityFieldThreads[] + ) + ) && !cell.value ? ( + + ) : null} + {getFieldThreadElement( + cell.row.cells[0].value, + 'description', + entityFieldThreads as EntityFieldThreads[], + onThreadLinkSelect, + EntityType.TABLE, + entityFqn, + `columns/${cell.row.cells[0].value}/description`, + Boolean(cell.value) )} - isOwner={hasEditAccess} - permission={Operation.UpdateDescription} - position="top"> - - + ) : null} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/FeedEditor/FeedEditor.tsx b/openmetadata-ui/src/main/resources/ui/src/components/FeedEditor/FeedEditor.tsx index 99bd535a5fb..f0b92a14c98 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/FeedEditor/FeedEditor.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/FeedEditor/FeedEditor.tsx @@ -21,6 +21,7 @@ import React, { useState, } from 'react'; import ReactQuill, { Quill } from 'react-quill'; +import { EditorPlaceHolder } from '../../constants/feed.constants'; import { HTMLToMarkdown, matcher } from '../../utils/FeedUtils'; import { insertMention, insertRef } from '../../utils/QuillUtils'; import { editorRef } from '../common/rich-text-editor/RichTextEditor.interface'; @@ -71,10 +72,10 @@ const modules = { const FeedEditor = forwardRef( ( - { className, editorClass, placeHolder, onChangeHandler }: FeedEditorProp, + { className, editorClass, onChangeHandler, defaultValue }: FeedEditorProp, ref ) => { - const [value, setValue] = useState(''); + const [value, setValue] = useState(defaultValue ?? ''); const handleOnChange = (value: string) => { setValue(value); @@ -97,7 +98,7 @@ const FeedEditor = forwardRef( { + header: string; + threadLink: string; + defaultValue?: string; + headerClassName?: string; + bodyClassName?: string; + onCancel: () => void; + createThread: (data: CreateThread) => void; +} + +const RequestDescriptionModal: FC = ({ + header, + headerClassName, + onCancel, + createThread, + threadLink, + defaultValue, +}) => { + const onPostThread = (value: string) => { + const currentUser = AppState.userDetails?.name ?? AppState.users[0]?.name; + const data = { + message: value, + from: currentUser, + about: threadLink, + }; + createThread(data); + onCancel(); + }; + + return ( + +
+
+
+

+ {header} +

+
+ + + +
+
+
+ +
+
+
+ ); +}; + +export default RequestDescriptionModal; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.test.tsx index f49b31bf662..22ad00db70d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/MyData.test.tsx @@ -22,10 +22,10 @@ import { import { SearchResponse } from 'Models'; import React, { ReactNode } from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { FeedFilter } from '../../enums/mydata.enum'; import { User } from '../../generated/entity/teams/user'; import { formatDataResponse } from '../../utils/APIUtils'; import MyDataPage from './MyData.component'; -import { FeedFilter } from '../../enums/mydata.enum'; const mockData = { data: { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx index c6a2e9f6c3b..3e47c2b4822 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.component.tsx @@ -13,13 +13,15 @@ import classNames from 'classnames'; import { compare } from 'fast-json-patch'; +import { isNil } from 'lodash'; import { EntityFieldThreads, EntityTags } from 'Models'; -import React, { useEffect, useState } from 'react'; +import React, { Fragment, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { getTeamDetailsPath } from '../../constants/constants'; +import { EntityType } from '../../enums/entity.enum'; import { Pipeline, Task } from '../../generated/entity/data/pipeline'; import { Operation } from '../../generated/entity/policies/accessControl/rule'; -import { User } from '../../generated/entity/teams/user'; +import { EntityReference, User } from '../../generated/entity/teams/user'; import { LabelType, State } from '../../generated/type/tagLabel'; import { useAuth } from '../../hooks/authHooks'; import { @@ -28,9 +30,13 @@ import { getUserTeams, isEven, } from '../../utils/CommonUtils'; -import { getFieldThreadElement } from '../../utils/FeedElementUtils'; +import { getEntityFeedLink } from '../../utils/EntityUtils'; +import { + getDefaultValue, + getFieldThreadElement, +} from '../../utils/FeedElementUtils'; import { getEntityFieldThreadCounts } from '../../utils/FeedUtils'; -import SVGIcons from '../../utils/SvgUtils'; +import SVGIcons, { Icons } from '../../utils/SvgUtils'; import { getTagsWithoutTier } from '../../utils/TableUtils'; import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList'; import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; @@ -43,6 +49,7 @@ import PageContainer from '../containers/PageContainer'; import Entitylineage from '../EntityLineage/EntityLineage.component'; import ManageTabComponent from '../ManageTab/ManageTab.component'; import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; +import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal'; import { PipeLineDetailsProp } from './PipelineDetails.interface'; const PipelineDetails = ({ @@ -83,6 +90,7 @@ const PipelineDetails = ({ feedCount, entityFieldThreadCount, createThread, + pipelineFQN, }: PipeLineDetailsProp) => { const { isAuthDisabled } = useAuth(); const [isEdit, setIsEdit] = useState(false); @@ -95,6 +103,15 @@ const PipelineDetails = ({ const [threadLink, setThreadLink] = useState(''); + const [selectedField, setSelectedField] = useState(''); + + const onEntityFieldSelect = (value: string) => { + setSelectedField(value); + }; + const closeRequestModal = () => { + setSelectedField(''); + }; + const hasEditAccess = () => { if (owner?.type === 'user') { return owner.id === getCurrentUserId(); @@ -311,7 +328,9 @@ const PipelineDetails = ({ 'tags', entityFieldThreadCount )} + entityFqn={pipelineFQN} entityName={entityName} + entityType={EntityType.PIPELINE} extraInfo={extraInfo} followHandler={followPipeline} followers={followersCount} @@ -345,7 +364,9 @@ const PipelineDetails = ({ 'description', entityFieldThreadCount )} + entityFqn={pipelineFQN} entityName={entityName} + entityType={EntityType.PIPELINE} hasEditAccess={hasEditAccess()} isEdit={isEdit} isReadOnly={deleted} @@ -353,6 +374,7 @@ const PipelineDetails = ({ onCancel={onCancel} onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} + onEntityFieldSelect={onEntityFieldSelect} onThreadLinkSelect={onThreadLinkSelect} /> @@ -403,40 +425,74 @@ const PipelineDetails = ({ /> ) : ( - No description added + No description added{' '} )} - {getFieldThreadElement( - task.name, - 'description', - getEntityFieldThreadCounts( - 'tasks', - entityFieldThreadCount - ) as EntityFieldThreads[], - onThreadLinkSelect - )} {!deleted && ( - + + + + {!isNil( + getFieldThreadElement( + task.name, + 'description', + getEntityFieldThreadCounts( + 'tasks', + entityFieldThreadCount + ) as EntityFieldThreads[], + onThreadLinkSelect + ) + ) && + onEntityFieldSelect && + !task.description ? ( + + ) : null} + {getFieldThreadElement( + task.name, + 'description', + getEntityFieldThreadCounts( + 'tasks', + entityFieldThreadCount + ) as EntityFieldThreads[], + onThreadLinkSelect, + EntityType.PIPELINE, + pipelineFQN, + `tasks/${task.name}/description`, + Boolean(task.description) )} - isOwner={hasEditAccess()} - permission={Operation.UpdateDescription} - position="top"> - - + )} @@ -453,15 +509,6 @@ const PipelineDetails = ({ )} - {threadLink ? ( - - ) : null} )} {activeTab === 2 && ( @@ -473,6 +520,7 @@ const PipelineDetails = ({ isEntityFeed withSidePanel className="" + entityName={entityName} feedList={entityThread} isLoading={isentityThreadLoading} postFeedHandler={postFeedHandler} @@ -519,6 +567,28 @@ const PipelineDetails = ({ onSave={onTaskUpdate} /> )} + {threadLink ? ( + + ) : null} + {selectedField ? ( + + ) : null} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.interface.ts index 0142ffa7b9b..64c206ecd13 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/PipelineDetails/PipelineDetails.interface.ts @@ -33,6 +33,7 @@ import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrum import { Edge, EdgeData } from '../EntityLineage/EntityLineage.interface'; export interface PipeLineDetailsProp { + pipelineFQN: string; version: string; isNodeLoading: LoadingNodeState; lineageLeafNodes: LeafNodes; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SchemaTab/SchemaTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SchemaTab/SchemaTab.component.tsx index b502161dff7..c8e19ab1e2a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SchemaTab/SchemaTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SchemaTab/SchemaTab.component.tsx @@ -30,8 +30,10 @@ type Props = { columnName: string; hasEditAccess?: boolean; isReadOnly?: boolean; + entityFqn?: string; entityFieldThreads?: EntityFieldThreads[]; onThreadLinkSelect?: (value: string) => void; + onEntityFieldSelect?: (value: string) => void; onUpdate?: (columns: Table['columns']) => void; }; @@ -44,7 +46,9 @@ const SchemaTab: FunctionComponent = ({ owner, entityFieldThreads, onThreadLinkSelect, + onEntityFieldSelect, isReadOnly = false, + entityFqn, }: Props) => { const [searchText, setSearchText] = useState(''); @@ -70,12 +74,14 @@ const SchemaTab: FunctionComponent = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx index 81067a7a97f..5cb0603982a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.component.tsx @@ -14,11 +14,14 @@ import { EntityTags } from 'Models'; import React, { useEffect, useState } from 'react'; import { getTeamDetailsPath } from '../../constants/constants'; +import { EntityType } from '../../enums/entity.enum'; import { Topic } from '../../generated/entity/data/topic'; -import { User } from '../../generated/entity/teams/user'; +import { EntityReference, User } from '../../generated/entity/teams/user'; import { LabelType, State } from '../../generated/type/tagLabel'; import { useAuth } from '../../hooks/authHooks'; import { getCurrentUserId, getUserTeams } from '../../utils/CommonUtils'; +import { getEntityFeedLink } from '../../utils/EntityUtils'; +import { getDefaultValue } from '../../utils/FeedElementUtils'; import { getEntityFieldThreadCounts } from '../../utils/FeedUtils'; import { bytesToSize } from '../../utils/StringsUtils'; import { getTagsWithoutTier } from '../../utils/TableUtils'; @@ -29,6 +32,7 @@ import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo'; import TabsPane from '../common/TabsPane/TabsPane'; import PageContainer from '../containers/PageContainer'; import ManageTabComponent from '../ManageTab/ManageTab.component'; +import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal'; import SchemaEditor from '../schema-editor/SchemaEditor'; import { TopicDetailsProps } from './TopicDetails.interface'; @@ -65,12 +69,22 @@ const TopicDetails: React.FC = ({ feedCount, entityFieldThreadCount, createThread, + topicFQN, }: TopicDetailsProps) => { const { isAuthDisabled } = useAuth(); const [isEdit, setIsEdit] = useState(false); const [followersCount, setFollowersCount] = useState(0); const [isFollowing, setIsFollowing] = useState(false); const [threadLink, setThreadLink] = useState(''); + const [selectedField, setSelectedField] = useState(''); + + const onEntityFieldSelect = (value: string) => { + setSelectedField(value); + }; + const closeRequestModal = () => { + setSelectedField(''); + }; + const hasEditAccess = () => { if (owner?.type === 'user') { return owner.id === getCurrentUserId(); @@ -309,7 +323,9 @@ const TopicDetails: React.FC = ({ 'tags', entityFieldThreadCount )} + entityFqn={topicFQN} entityName={entityName} + entityType={EntityType.TOPIC} extraInfo={extraInfo} followHandler={followTopic} followers={followersCount} @@ -343,7 +359,9 @@ const TopicDetails: React.FC = ({ 'description', entityFieldThreadCount )} + entityFqn={topicFQN} entityName={entityName} + entityType={EntityType.TOPIC} hasEditAccess={hasEditAccess()} isEdit={isEdit} isReadOnly={deleted} @@ -351,6 +369,7 @@ const TopicDetails: React.FC = ({ onCancel={onCancel} onDescriptionEdit={onDescriptionEdit} onDescriptionUpdate={onDescriptionUpdate} + onEntityFieldSelect={onEntityFieldSelect} onThreadLinkSelect={onThreadLinkSelect} /> @@ -359,15 +378,6 @@ const TopicDetails: React.FC = ({
- {threadLink ? ( - - ) : null} )} {activeTab === 2 && ( @@ -379,6 +389,7 @@ const TopicDetails: React.FC = ({ isEntityFeed withSidePanel className="" + entityName={entityName} feedList={entityThread} isLoading={isentityThreadLoading} postFeedHandler={postFeedHandler} @@ -402,6 +413,28 @@ const TopicDetails: React.FC = ({ )} + {threadLink ? ( + + ) : null} + {selectedField ? ( + + ) : null} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.interface.ts index 27235496e31..4d5bf38d47b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/TopicDetails/TopicDetails.interface.ts @@ -24,6 +24,7 @@ import { TagLabel } from '../../generated/type/tagLabel'; import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface'; export interface TopicDetailsProps { + topicFQN: string; version?: string; schemaText: string; schemaType: string; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/description/Description.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/description/Description.tsx index 0b0fdd5fc5d..4c5226749c1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/description/Description.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/description/Description.tsx @@ -14,11 +14,12 @@ import classNames from 'classnames'; import { isUndefined } from 'lodash'; import { EntityFieldThreads } from 'Models'; -import React from 'react'; +import React, { Fragment } from 'react'; import { Table } from '../../../generated/entity/data/table'; import { Operation } from '../../../generated/entity/policies/accessControl/rule'; import { getHtmlForNonAdminAction } from '../../../utils/CommonUtils'; -import SVGIcons from '../../../utils/SvgUtils'; +import { getEntityFeedLink } from '../../../utils/EntityUtils'; +import SVGIcons, { Icons } from '../../../utils/SvgUtils'; import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor'; import NonAdminAction from '../non-admin-action/NonAdminAction'; import RichTextEditorPreviewer from '../rich-text-editor/RichTextEditorPreviewer'; @@ -32,12 +33,15 @@ type Props = { description: string; isEdit?: boolean; isReadOnly?: boolean; + entityType?: string; + entityFqn?: string; entityFieldThreads?: EntityFieldThreads[]; onThreadLinkSelect?: (value: string) => void; onDescriptionEdit?: () => void; onCancel?: () => void; onDescriptionUpdate?: (value: string) => void; onSuggest?: (value: string) => void; + onEntityFieldSelect?: (value: string) => void; }; const Description = ({ @@ -54,6 +58,9 @@ const Description = ({ entityName, entityFieldThreads, onThreadLinkSelect, + onEntityFieldSelect, + entityType, + entityFqn, }: Props) => { const descriptionThread = entityFieldThreads?.[0]; @@ -78,19 +85,9 @@ const Description = ({ /> ) : ( - No description added + No description added{' '} )} - {!isUndefined(descriptionThread) ? ( -

- onThreadLinkSelect?.(descriptionThread.entityLink) - }> - {descriptionThread.count}{' '} - threads -

- ) : null} {isEdit && ( + {isUndefined(descriptionThread) && + onEntityFieldSelect && + !description?.trim() ? ( + + ) : null} + {!isUndefined(descriptionThread) ? ( +

+ onThreadLinkSelect?.(descriptionThread.entityLink) + }> + + {' '} + {descriptionThread.count} + +

+ ) : ( + + {description?.trim() && onThreadLinkSelect ? ( +

+ onThreadLinkSelect?.( + getEntityFeedLink(entityType, entityFqn, 'description') + ) + }> + +

+ ) : null} +
+ )} ) : null} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/EntityPageInfo.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/EntityPageInfo.tsx index 7a60fed0b9c..b2f0e219518 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/EntityPageInfo.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/entityPageInfo/EntityPageInfo.tsx @@ -20,8 +20,8 @@ import { Operation } from '../../../generated/entity/policies/accessControl/rule import { User } from '../../../generated/entity/teams/user'; import { TagLabel } from '../../../generated/type/tagLabel'; import { getHtmlForNonAdminAction } from '../../../utils/CommonUtils'; -import { getInfoElements } from '../../../utils/EntityUtils'; -import SVGIcons from '../../../utils/SvgUtils'; +import { getEntityFeedLink, getInfoElements } from '../../../utils/EntityUtils'; +import SVGIcons, { Icons } from '../../../utils/SvgUtils'; import { getFollowerDetail } from '../../../utils/TableUtils'; import { getTagCategories, getTaglist } from '../../../utils/TagsUtils'; import TagsContainer from '../../tags-container/tags-container'; @@ -46,6 +46,8 @@ type Props = { hasEditAccess?: boolean; followersList: Array; entityName: string; + entityType?: string; + entityFqn?: string; version?: string; isVersionSelected?: boolean; entityFieldThreads?: EntityFieldThreads[]; @@ -75,6 +77,8 @@ const EntityPageInfo = ({ versionHandler, entityFieldThreads, onThreadLinkSelect, + entityFqn, + entityType, }: Props) => { const tagThread = entityFieldThreads?.[0]; const [isEditable, setIsEditable] = useState(false); @@ -407,11 +411,28 @@ const EntityPageInfo = ({ {!isUndefined(tagThread) ? (

onThreadLinkSelect?.(tagThread.entityLink)}> - {tagThread.count} threads + + + {tagThread.count} +

- ) : null} + ) : ( +

+ onThreadLinkSelect?.( + getEntityFeedLink(entityType, entityFqn, 'tags') + ) + }> + +

+ )} )} diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/feed.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/feed.constants.ts index 158063e5ab5..76f9ab63193 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/feed.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/feed.constants.ts @@ -20,3 +20,11 @@ export const hashtagRegEx = /\[#(.+?)?\]\((.+?)?\)/g; export const linkRegEx = /\((.+?\/\/.+?)\/(.+?)\/(.+?)\)/; export const entityLinkRegEx = /<#E\/([^<>]+?)\/([^<>]+?)>/g; export const entityRegex = /<#E\/([^<>]+?)\/([^<>]+?)\|(\[(.+?)?\]\((.+?)?\))>/; + +export const entityUrlMap = { + team: 'teams', + user: 'user', +}; + +export const EditorPlaceHolder = `Use @mention to tag a user or a team. +Use #mention to tag a data asset.`; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx index c0f02b75567..7fbcf46b002 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DashboardDetailsPage/DashboardDetailsPage.component.tsx @@ -141,6 +141,14 @@ const DashboardDetailsPage = () => { }); } }; + const getEntityFeedCount = () => { + getFeedCount(getEntityFeedLink(EntityType.DASHBOARD, dashboardFQN)).then( + (res: AxiosResponse) => { + setFeedCount(res.data.totalCount); + setEntityFieldThreadCount(res.data.counts); + } + ); + }; useEffect(() => { if (dashboardDetailsTabs[activeTab - 1].path !== tab) { @@ -347,6 +355,7 @@ const DashboardDetailsPage = () => { setCurrentVersion(version); setDashboardDetails(res.data); setDescription(description); + getEntityFeedCount(); }) .catch((err: AxiosError) => { const errMsg = @@ -401,6 +410,7 @@ const DashboardDetailsPage = () => { setTier(getTierTags(res.data.tags)); setCurrentVersion(res.data.version); setTags(getTagsWithoutTier(res.data.tags)); + getEntityFeedCount(); }) .catch((err: AxiosError) => { const errMsg = @@ -422,6 +432,7 @@ const DashboardDetailsPage = () => { setCurrentVersion(res.data.version); setOwner(res.data.owner); setTier(getTierTags(res.data.tags)); + getEntityFeedCount(); resolve(); }) .catch((err: AxiosError) => { @@ -540,27 +551,20 @@ const DashboardDetailsPage = () => { }); }; - const getEntityFeedCount = () => { - getFeedCount(getEntityFeedLink(EntityType.DASHBOARD, dashboardFQN)).then( - (res: AxiosResponse) => { - setFeedCount(res.data.totalCount); - setEntityFieldThreadCount(res.data.counts); - } - ); - }; const createThread = (data: CreateThread) => { postThread(data) .then((res: AxiosResponse) => { setEntityThread((pre) => [...pre, res.data]); + getEntityFeedCount(); showToast({ variant: 'success', - body: 'Thread is created successfully', + body: 'Conversation created successfully', }); }) .catch(() => { showToast({ variant: 'error', - body: 'Error while creating thread', + body: 'Error while creating the conversation', }); }); }; @@ -595,6 +599,7 @@ const DashboardDetailsPage = () => { charts={charts} createThread={createThread} dashboardDetails={dashboardDetails} + dashboardFQN={dashboardFQN} dashboardTags={tags} dashboardUrl={dashboardUrl} deleted={deleted} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatasetDetailsPage/DatasetDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatasetDetailsPage/DatasetDetailsPage.component.tsx index f82255030ad..771a3c38969 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatasetDetailsPage/DatasetDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatasetDetailsPage/DatasetDetailsPage.component.tsx @@ -574,15 +574,16 @@ const DatasetDetailsPage: FunctionComponent = () => { postThread(data) .then((res: AxiosResponse) => { setEntityThread((pre) => [...pre, res.data]); + getEntityFeedCount(); showToast({ variant: 'success', - body: 'Thread is created successfully', + body: 'Conversation created successfully', }); }) .catch(() => { showToast({ variant: 'error', - body: 'Error while creating thread', + body: 'Error while creating the conversation', }); }); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx index 95efd95ed63..dee330da27d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PipelineDetails/PipelineDetailsPage.component.tsx @@ -144,6 +144,15 @@ const PipelineDetailsPage = () => { } }; + const getEntityFeedCount = () => { + getFeedCount(getEntityFeedLink(EntityType.PIPELINE, pipelineFQN)).then( + (res: AxiosResponse) => { + setFeedCount(res.data.totalCount); + setEntityFieldThreadCount(res.data.counts); + } + ); + }; + useEffect(() => { if (pipelineDetailsTabs[activeTab - 1].path !== tab) { setActiveTab(getCurrentPipelineTab(tab)); @@ -331,6 +340,7 @@ const PipelineDetailsPage = () => { setCurrentVersion(version); setPipelineDetails(res.data); setDescription(description); + getEntityFeedCount(); }) .catch((err: AxiosError) => { const errMsg = @@ -350,6 +360,7 @@ const PipelineDetailsPage = () => { setCurrentVersion(res.data.version); setOwner(res.data.owner); setTier(getTierTags(res.data.tags)); + getEntityFeedCount(); resolve(); }) .catch((err: AxiosError) => { @@ -370,6 +381,7 @@ const PipelineDetailsPage = () => { setTier(getTierTags(res.data.tags)); setCurrentVersion(res.data.version); setTags(getTagsWithoutTier(res.data.tags)); + getEntityFeedCount(); }) .catch((err: AxiosError) => { const errMsg = @@ -384,6 +396,7 @@ const PipelineDetailsPage = () => { const onTaskUpdate = (jsonPatch: Array) => { patchPipelineDetails(pipelineId, jsonPatch).then((res: AxiosResponse) => { setTasks(res.data.tasks || []); + getEntityFeedCount(); }); }; @@ -483,27 +496,20 @@ const PipelineDetailsPage = () => { }); }; - const getEntityFeedCount = () => { - getFeedCount(getEntityFeedLink(EntityType.PIPELINE, pipelineFQN)).then( - (res: AxiosResponse) => { - setFeedCount(res.data.totalCount); - setEntityFieldThreadCount(res.data.counts); - } - ); - }; const createThread = (data: CreateThread) => { postThread(data) .then((res: AxiosResponse) => { setEntityThread((pre) => [...pre, res.data]); + getEntityFeedCount(); showToast({ variant: 'success', - body: 'Thread is created successfully', + body: 'Conversation created successfully', }); }) .catch(() => { showToast({ variant: 'error', - body: 'Error while creating thread', + body: 'Error while creating the conversation', }); }); }; @@ -552,6 +558,7 @@ const PipelineDetailsPage = () => { loadNodeHandler={loadNodeHandler} owner={owner} pipelineDetails={pipelineDetails} + pipelineFQN={pipelineFQN} pipelineTags={tags} pipelineUrl={pipelineUrl} postFeedHandler={postFeedHandler} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx index 866d39884ad..f9ad15d4600 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TopicDetails/TopicDetailsPage.component.tsx @@ -115,6 +115,15 @@ const TopicDetailsPage: FunctionComponent = () => { } }; + const getEntityFeedCount = () => { + getFeedCount(getEntityFeedLink(EntityType.TOPIC, topicFQN)).then( + (res: AxiosResponse) => { + setFeedCount(res.data.totalCount); + setEntityFieldThreadCount(res.data.counts); + } + ); + }; + const fetchActivityFeed = () => { setIsentityThreadLoading(true); getAllFeeds(getEntityFeedLink(EntityType.TOPIC, topicFQN)) @@ -273,6 +282,7 @@ const TopicDetailsPage: FunctionComponent = () => { setCurrentVersion(version); setTopicDetails(res.data); setDescription(description); + getEntityFeedCount(); }) .catch((err: AxiosError) => { const errMsg = @@ -292,6 +302,7 @@ const TopicDetailsPage: FunctionComponent = () => { setCurrentVersion(res.data.version); setOwner(res.data.owner); setTier(getTierTags(res.data.tags)); + getEntityFeedCount(); resolve(); }) .catch((err: AxiosError) => { @@ -312,6 +323,7 @@ const TopicDetailsPage: FunctionComponent = () => { setTier(getTierTags(res.data.tags)); setCurrentVersion(res.data.version); setTags(getTagsWithoutTier(res.data.tags)); + getEntityFeedCount(); }) .catch((err: AxiosError) => { const errMsg = @@ -359,27 +371,20 @@ const TopicDetailsPage: FunctionComponent = () => { }); }; - const getEntityFeedCount = () => { - getFeedCount(getEntityFeedLink(EntityType.TOPIC, topicFQN)).then( - (res: AxiosResponse) => { - setFeedCount(res.data.totalCount); - setEntityFieldThreadCount(res.data.counts); - } - ); - }; const createThread = (data: CreateThread) => { postThread(data) .then((res: AxiosResponse) => { setEntityThread((pre) => [...pre, res.data]); + getEntityFeedCount(); showToast({ variant: 'success', - body: 'Thread is created successfully', + body: 'Conversation created successfully', }); }) .catch(() => { showToast({ variant: 'error', - body: 'Error while creating thread', + body: 'Error while creating the conversation', }); }); }; @@ -429,6 +434,7 @@ const TopicDetailsPage: FunctionComponent = () => { tagUpdateHandler={onTagUpdate} tier={tier as TagLabel} topicDetails={topicDetails} + topicFQN={topicFQN} topicTags={tags} unfollowTopicHandler={unfollowTopic} users={AppState.users} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/database-details/index.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/database-details/index.tsx index b7a6e06d07d..38fe8bc4aa6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/database-details/index.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/database-details/index.tsx @@ -16,7 +16,12 @@ import classNames from 'classnames'; import { compare } from 'fast-json-patch'; import { isNil } from 'lodash'; import { observer } from 'mobx-react'; -import { EntityThread, ExtraInfo, Paging } from 'Models'; +import { + EntityFieldThreadCount, + EntityThread, + ExtraInfo, + Paging, +} from 'Models'; import React, { FunctionComponent, useEffect, useRef, useState } from 'react'; import { Link, useHistory, useParams } from 'react-router-dom'; import { default as AppState, default as appState } from '../../AppState'; @@ -28,9 +33,11 @@ import { getAllFeeds, getFeedCount, postFeedById, + postThread, } from '../../axiosAPIs/feedsAPI'; import { getDatabaseTables } from '../../axiosAPIs/tableAPI'; import ActivityFeedList from '../../components/ActivityFeed/ActivityFeedList/ActivityFeedList'; +import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import Description from '../../components/common/description/Description'; import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder'; import NextPrevious from '../../components/common/next-previous/NextPrevious'; @@ -41,6 +48,7 @@ import { TitleBreadcrumbProps } from '../../components/common/title-breadcrumb/t import PageContainer from '../../components/containers/PageContainer'; import Loader from '../../components/Loader/Loader'; import ManageTabComponent from '../../components/ManageTab/ManageTab.component'; +import RequestDescriptionModal from '../../components/Modals/RequestDescriptionModal/RequestDescriptionModal'; import Tags from '../../components/tags/tags'; import { getDatabaseDetailsPath, @@ -52,8 +60,10 @@ import { } from '../../constants/constants'; import { EntityType, TabSpecificField } from '../../enums/entity.enum'; import { ServiceCategory } from '../../enums/service.enum'; +import { CreateThread } from '../../generated/api/feed/createThread'; import { Database } from '../../generated/entity/data/database'; import { Table } from '../../generated/entity/data/table'; +import { EntityReference } from '../../generated/entity/teams/user'; import useToastContext from '../../hooks/useToastContext'; import { getEntityMissingError, @@ -65,6 +75,8 @@ import { getCurrentDatabaseDetailsTab, } from '../../utils/DatabaseDetailsUtils'; import { getEntityFeedLink, getInfoElements } from '../../utils/EntityUtils'; +import { getDefaultValue } from '../../utils/FeedElementUtils'; +import { getEntityFieldThreadCounts } from '../../utils/FeedUtils'; import { serviceTypeLogo } from '../../utils/ServiceUtils'; import { getOwnerFromId, getUsagePercentile } from '../../utils/TableUtils'; import { getTableTags } from '../../utils/TagsUtils'; @@ -100,6 +112,12 @@ const DatabaseDetails: FunctionComponent = () => { const [isentityThreadLoading, setIsentityThreadLoading] = useState(false); const [feedCount, setFeedCount] = useState(0); + const [entityFieldThreadCount, setEntityFieldThreadCount] = useState< + EntityFieldThreadCount[] + >([]); + + const [threadLink, setThreadLink] = useState(''); + const [selectedField, setSelectedField] = useState(''); const history = useHistory(); const isMounting = useRef(true); @@ -187,6 +205,35 @@ const DatabaseDetails: FunctionComponent = () => { }); }; + const onThreadLinkSelect = (link: string) => { + setThreadLink(link); + }; + + const onThreadPanelClose = () => { + setThreadLink(''); + }; + + const onEntityFieldSelect = (value: string) => { + setSelectedField(value); + }; + const closeRequestModal = () => { + setSelectedField(''); + }; + + const getEntityFeedCount = () => { + getFeedCount(getEntityFeedLink(EntityType.DATABASE, databaseFQN)) + .then((res: AxiosResponse) => { + setFeedCount(res.data.totalCount); + setEntityFieldThreadCount(res.data.counts); + }) + .catch(() => { + showToast({ + variant: 'error', + body: 'Error while fetching feed count', + }); + }); + }; + const getDetailsByFQN = () => { getDatabaseDetailsByFQN(databaseFQN, ['owner']) .then((res: AxiosResponse) => { @@ -260,6 +307,7 @@ const DatabaseDetails: FunctionComponent = () => { setDatabase(updatedDatabaseDetails); setDescription(updatedHTML); setIsEdit(false); + getEntityFeedCount(); }) .catch((err: AxiosError) => { const errMsg = @@ -367,15 +415,21 @@ const DatabaseDetails: FunctionComponent = () => { }); }); }; - const getEntityFeedCount = () => { - getFeedCount(getEntityFeedLink(EntityType.DATABASE, databaseFQN)) + + const createThread = (data: CreateThread) => { + postThread(data) .then((res: AxiosResponse) => { - setFeedCount(res.data.totalCount); + setEntityThread((pre) => [...pre, res.data]); + getEntityFeedCount(); + showToast({ + variant: 'success', + body: 'Conversation created successfully', + }); }) .catch(() => { showToast({ variant: 'error', - body: 'Error while fetching feed count', + body: 'Error while creating the conversation', }); }); }; @@ -448,11 +502,19 @@ const DatabaseDetails: FunctionComponent = () => {
@@ -605,6 +667,7 @@ const DatabaseDetails: FunctionComponent = () => { isEntityFeed withSidePanel className="" + entityName={databaseName} feedList={entityThread} isLoading={isentityThreadLoading} postFeedHandler={postFeedHandler} @@ -625,6 +688,30 @@ const DatabaseDetails: FunctionComponent = () => { )}
+ {threadLink ? ( + + ) : null} + {selectedField ? ( + + ) : null} )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/service/index.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/service/index.tsx index 98a01708457..b186cb4ca78 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/service/index.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/service/index.tsx @@ -15,7 +15,13 @@ import { AxiosError, AxiosResponse } from 'axios'; import classNames from 'classnames'; import { compare } from 'fast-json-patch'; import { isNil, isUndefined } from 'lodash'; -import { EntityThread, ExtraInfo, Paging, ServicesData } from 'Models'; +import { + EntityFieldThreadCount, + EntityThread, + ExtraInfo, + Paging, + ServicesData, +} from 'Models'; import React, { Fragment, FunctionComponent, useEffect, useState } from 'react'; import { Link, useHistory, useParams } from 'react-router-dom'; import AppState from '../../AppState'; @@ -32,11 +38,13 @@ import { getAllFeeds, getFeedCount, postFeedById, + postThread, } from '../../axiosAPIs/feedsAPI'; import { getPipelines } from '../../axiosAPIs/pipelineAPI'; import { getServiceByFQN, updateService } from '../../axiosAPIs/serviceAPI'; import { getTopics } from '../../axiosAPIs/topicsAPI'; import ActivityFeedList from '../../components/ActivityFeed/ActivityFeedList/ActivityFeedList'; +import ActivityThreadPanel from '../../components/ActivityFeed/ActivityThreadPanel/ActivityThreadPanel'; import Description from '../../components/common/description/Description'; import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder'; import IngestionError from '../../components/common/error/IngestionError'; @@ -49,6 +57,7 @@ import PageContainer from '../../components/containers/PageContainer'; import Ingestion from '../../components/Ingestion/Ingestion.component'; import Loader from '../../components/Loader/Loader'; import ManageTabComponent from '../../components/ManageTab/ManageTab.component'; +import RequestDescriptionModal from '../../components/Modals/RequestDescriptionModal/RequestDescriptionModal'; import ServiceConfig from '../../components/ServiceConfig/ServiceConfig'; import Tags from '../../components/tags/tags'; import { @@ -59,6 +68,7 @@ import { import { TabSpecificField } from '../../enums/entity.enum'; import { SearchIndex } from '../../enums/search.enum'; import { ServiceCategory } from '../../enums/service.enum'; +import { CreateThread } from '../../generated/api/feed/createThread'; import { Dashboard } from '../../generated/entity/data/dashboard'; import { Database } from '../../generated/entity/data/database'; import { Pipeline } from '../../generated/entity/data/pipeline'; @@ -81,6 +91,8 @@ import { isEven, } from '../../utils/CommonUtils'; import { getEntityFeedLink, getInfoElements } from '../../utils/EntityUtils'; +import { getDefaultValue } from '../../utils/FeedElementUtils'; +import { getEntityFieldThreadCounts } from '../../utils/FeedUtils'; import { getCurrentServiceTab, getIsIngestionEnable, @@ -130,6 +142,19 @@ const ServicePage: FunctionComponent = () => { const [isentityThreadLoading, setIsentityThreadLoading] = useState(false); const [feedCount, setFeedCount] = useState(0); + const [entityFieldThreadCount, setEntityFieldThreadCount] = useState< + EntityFieldThreadCount[] + >([]); + + const [threadLink, setThreadLink] = useState(''); + const [selectedField, setSelectedField] = useState(''); + + const onEntityFieldSelect = (value: string) => { + setSelectedField(value); + }; + const closeRequestModal = () => { + setSelectedField(''); + }; const getCountLabel = () => { switch (serviceName) { @@ -239,6 +264,28 @@ const ServicePage: FunctionComponent = () => { } }; + const onThreadLinkSelect = (link: string) => { + setThreadLink(link); + }; + + const onThreadPanelClose = () => { + setThreadLink(''); + }; + + const getEntityFeedCount = () => { + getFeedCount(getEntityFeedLink(serviceCategory.slice(0, -1), serviceFQN)) + .then((res: AxiosResponse) => { + setFeedCount(res.data.totalCount); + setEntityFieldThreadCount(res.data.counts); + }) + .catch(() => { + showToast({ + variant: 'error', + body: 'Error while fetching feed count', + }); + }); + }; + const getSchemaFromType = (type: AirflowPipeline['pipelineType']) => { switch (type) { case PipelineType.Metadata: @@ -254,7 +301,7 @@ const ServicePage: FunctionComponent = () => { const getAllIngestionWorkflows = (paging?: string) => { setIsloading(true); - getAirflowPipelines(['owner, tags, status'], serviceFQN, '', paging) + getAirflowPipelines(['owner'], serviceFQN, '', paging) .then((res) => { if (res.data.data) { setIngestions(res.data.data); @@ -753,10 +800,12 @@ const ServicePage: FunctionComponent = () => { const onDescriptionUpdate = (updatedHTML: string) => { if (description !== updatedHTML && !isUndefined(serviceDetails)) { - const { id } = serviceDetails; + const { id, ...restDetails } = serviceDetails; const updatedServiceDetails = { - ...serviceDetails, + databaseConnection: restDetails.databaseConnection, + name: restDetails.name, + serviceType: restDetails.serviceType, description: updatedHTML, }; @@ -765,6 +814,7 @@ const ServicePage: FunctionComponent = () => { setDescription(updatedHTML); setServiceDetails(updatedServiceDetails); setIsEdit(false); + getEntityFeedCount(); }) .catch((err: AxiosError) => { const errMsg = err.message || 'Something went wrong!'; @@ -865,15 +915,20 @@ const ServicePage: FunctionComponent = () => { }); }; - const getEntityFeedCount = () => { - getFeedCount(getEntityFeedLink(serviceCategory.slice(0, -1), serviceFQN)) + const createThread = (data: CreateThread) => { + postThread(data) .then((res: AxiosResponse) => { - setFeedCount(res.data.totalCount); + setEntityThread((pre) => [...pre, res.data]); + getEntityFeedCount(); + showToast({ + variant: 'success', + body: 'Conversation created successfully', + }); }) .catch(() => { showToast({ variant: 'error', - body: 'Error while fetching feed count', + body: 'Error while creating the conversation', }); }); }; @@ -922,11 +977,19 @@ const ServicePage: FunctionComponent = () => { @@ -1017,6 +1080,7 @@ const ServicePage: FunctionComponent = () => { isEntityFeed withSidePanel className="" + entityName={serviceFQN} feedList={entityThread} isLoading={isentityThreadLoading} postFeedHandler={postFeedHandler} @@ -1075,6 +1139,30 @@ const ServicePage: FunctionComponent = () => { )} + {threadLink ? ( + + ) : null} + {selectedField ? ( + + ) : null} )} diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx index 59a8cb1e97e..e6ff9552d95 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/tour-page/TourPage.component.tsx @@ -30,6 +30,7 @@ import { mockFeedData, mockSearchData as exploreSearchData, } from '../../constants/mockTourData.constants'; +import { FeedFilter } from '../../enums/mydata.enum'; import { CurrentTourPageType } from '../../enums/tour.enum'; import { Table, @@ -39,7 +40,6 @@ import { import { TagLabel } from '../../generated/type/tagLabel'; import { useTour } from '../../hooks/useTour'; import { getSteps } from '../../utils/TourUtils'; -import { FeedFilter } from '../../enums/mydata.enum'; const mockData = { data: { hits: { hits: [] } }, 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 a77667e44a8..b69990241c9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx @@ -471,9 +471,10 @@ export const getInfoElements = (data: ExtraInfo) => { export const getEntityFeedLink: Function = ( type: string, - fqn: string + fqn: string, + field?: string ): string | undefined => { if (isUndefined(type) || isUndefined(fqn)) return undefined; - return `<#E/${type}/${fqn}>`; + return `<#E/${type}/${fqn}${field ? `/${field}` : ''}>`; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx index 1749d5e6254..e56f1dd8311 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx @@ -26,7 +26,7 @@ import { FieldChange, } from '../generated/entity/services/databaseService'; import { TagLabel } from '../generated/type/tagLabel'; -import { Paragraph, UnOrderedList } from './MarkdownUtils'; +import { Paragraph, Span, UnOrderedList } from './MarkdownUtils'; import { isValidJSONString } from './StringsUtils'; import { getEntityLink, getOwnerFromId } from './TableUtils'; @@ -34,7 +34,7 @@ import { getEntityLink, getOwnerFromId } from './TableUtils'; const parseMarkdown = ( content: string, className: string, - isNewLine: boolean + _isNewLine: boolean ) => { return ( void + onThreadLinkSelect?: (value: string) => void, + entityType?: string, + entityFqn?: string, + entityField?: string, + flag = true ) => { let threadValue: EntityFieldThreads = {} as EntityFieldThreads; @@ -33,13 +40,46 @@ export const getFieldThreadElement = ( return !isEmpty(threadValue) ? (

{ e.preventDefault(); e.stopPropagation(); onThreadLinkSelect?.(threadValue.entityLink); }}> - {threadValue.count} threads + + + {threadValue.count} +

- ) : null; + ) : ( + + {entityType && entityFqn && entityField && flag ? ( +

{ + e.preventDefault(); + e.stopPropagation(); + onThreadLinkSelect?.( + getEntityFeedLink(entityType, entityFqn, entityField) + ); + }}> + +

+ ) : null} +
+ ); +}; + +export const getDefaultValue = (owner: EntityReference) => { + const message = 'Can you add a description?'; + if (isUndefined(owner)) { + return `${message}`; + } else { + const name = owner.name; + const displayName = owner.displayName; + const entityType = owner.type; + const mention = `@${displayName}`; + + return `${mention} ${message}`; + } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.ts index ef297ea9c70..0e870c0204d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/FeedUtils.ts @@ -28,6 +28,7 @@ import { entityLinkRegEx, entityRegex, EntityRegEx, + entityUrlMap, hashtagRegEx, linkRegEx, mentionRegEx, @@ -78,11 +79,15 @@ export const HTMLToMarkdown = new TurndownService({ }, }); -export const getReplyText = (count: number) => { - if (count === 0) return 'Reply in thread'; - if (count === 1) return `${count} Reply`; +export const getReplyText = ( + count: number, + singular?: string, + plural?: string +) => { + if (count === 0) return 'Reply in conversation'; + if (count === 1) return `${count} ${singular ? singular : 'older reply'}`; - return `${count} Replies`; + return `${count} ${plural ? plural : 'older replies'}`; }; export const getEntityFieldThreadCounts = ( @@ -109,6 +114,23 @@ export const getThreadField = (value: string, separator = '/') => { return value.split(separator).slice(-2); }; +export const getThreadValue = ( + columnName: string, + columnField: string, + entityFieldThreads: EntityFieldThreads[] +) => { + let threadValue; + + entityFieldThreads?.forEach((thread) => { + const threadField = getThreadField(thread.entityField); + if (threadField[0] === columnName && threadField[1] === columnField) { + threadValue = thread; + } + }); + + return threadValue; +}; + export async function suggestions(searchTerm: string, mentionChar: string) { if (mentionChar === '@') { let atValues = []; @@ -117,10 +139,14 @@ export async function suggestions(searchTerm: string, mentionChar: string) { const hits = data.data.hits.hits; // eslint-disable-next-line @typescript-eslint/no-explicit-any atValues = hits.map((hit: any) => { + const entityType = hit._source.entity_type; + return { id: hit._id, value: `@${hit._source.display_name}`, - link: `${document.location.protocol}//${document.location.host}/${hit._source.entity_type}/${hit._source.name}`, + link: `${document.location.protocol}//${document.location.host}/${ + entityUrlMap[entityType as keyof typeof entityUrlMap] + }/${hit._source.name}`, }; }); } else { @@ -128,10 +154,14 @@ export async function suggestions(searchTerm: string, mentionChar: string) { const hits = data.data.suggest['table-suggest'][0]['options']; // eslint-disable-next-line @typescript-eslint/no-explicit-any atValues = hits.map((hit: any) => { + const entityType = hit._source.entity_type; + return { id: hit._id, value: `@${hit._source.display_name}`, - link: `${document.location.protocol}//${document.location.host}/${hit._source.entity_type}/${hit._source.name}`, + link: `${document.location.protocol}//${document.location.host}/${ + entityUrlMap[entityType as keyof typeof entityUrlMap] + }/${hit._source.name}`, }; }); } @@ -202,10 +232,12 @@ export const getBackendFormat = (message: string) => { const hashtagList = [...new Set(getHashTagList(message) ?? [])]; const mentionDetails = mentionList.map((m) => getEntityDetail(m) ?? []); const hashtagDetails = hashtagList.map((h) => getEntityDetail(h) ?? []); + const urlEntries = Object.entries(entityUrlMap); mentionList.forEach((m, i) => { const updatedDetails = mentionDetails[i].slice(-2); - const entityLink = `<#E/${updatedDetails[0]}/${updatedDetails[1]}|${m}>`; + const entityType = urlEntries.find((e) => e[1] === updatedDetails[0])?.[0]; + const entityLink = `<#E/${entityType}/${updatedDetails[1]}|${m}>`; updatedMessage = updatedMessage.replaceAll(m, entityLink); }); hashtagList.forEach((h, i) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/MarkdownUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/MarkdownUtils.tsx index 414c1824245..801955ab276 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/MarkdownUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/MarkdownUtils.tsx @@ -15,8 +15,11 @@ import React, { FC, HTMLAttributes } from 'react'; export const Paragraph: FC< HTMLAttributes & { isNewLine: boolean } -> = ({ children, isNewLine = true, ...props }) => - isNewLine ?

{children}

: {children}; +> = ({ children, ...props }) =>

{children}

; + +export const Span: FC< + HTMLAttributes & { isNewLine: boolean } +> = ({ children, ...props }) => {children}; export const UnOrderedList: FC> = ({ children, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx index 11e5274c69e..cc0df9e5488 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SvgUtils.tsx @@ -18,6 +18,7 @@ import IconGithub from '../assets/img/icon-github.png'; import IconGoogle from '../assets/img/icon-google.png'; import IconOkta from '../assets/img/icon-okta.png'; import IconWelcomePopper from '../assets/img/welcome-popper-icon.png'; +import IconCommentPlus from '../assets/svg/add-chat.svg'; import IconAnnouncementWhite from '../assets/svg/announcements-white.svg'; import IconAnnouncement from '../assets/svg/announcements.svg'; import IconAPI from '../assets/svg/api.svg'; @@ -25,6 +26,7 @@ import IconArrowDownPrimary from '../assets/svg/arrow-down-primary.svg'; import IconArrowRightPrimary from '../assets/svg/arrow-right-primary.svg'; import IconSuccess from '../assets/svg/check.svg'; import IconCheckboxPrimary from '../assets/svg/checkbox-primary.svg'; +import IconComments from '../assets/svg/comment.svg'; import IconConfigColor from '../assets/svg/config-color.svg'; import IconConfig from '../assets/svg/config.svg'; import IconControlMinus from '../assets/svg/control-minus.svg'; @@ -95,6 +97,7 @@ import LogoMonogram from '../assets/svg/logo-monogram.svg'; import Logo from '../assets/svg/logo.svg'; import IconManageColor from '../assets/svg/manage-color.svg'; import IconMinus from '../assets/svg/minus.svg'; +import IconPaperPlanePrimary from '../assets/svg/paper-plane-primary.svg'; import IconPaperPlane from '../assets/svg/paper-plane.svg'; import IconPipelineGrey from '../assets/svg/pipeline-grey.svg'; import IconPipeline from '../assets/svg/pipeline.svg'; @@ -102,6 +105,7 @@ import IconPlus from '../assets/svg/plus.svg'; import IconProfilerColor from '../assets/svg/profiler-color.svg'; import IconProfiler from '../assets/svg/profiler.svg'; import IconHelpCircle from '../assets/svg/question-circle.svg'; +import IconRequest from '../assets/svg/request-icon.svg'; import IconSampleDataColor from '../assets/svg/sample-data-colored.svg'; import IconSampleData from '../assets/svg/sample-data.svg'; import IconSchemaColor from '../assets/svg/schema-color.svg'; @@ -238,6 +242,7 @@ export const Icons = { CONTROLMINUS: 'icon-control-minus', EDITLINEAGECOLOR: 'icon-edit-lineage-color', EDITLINEAGE: 'icon-edit-lineage', + REQUEST: 'icon-request', CHECKBOX_PRIMARY: 'icon-checkbox-primary', ARROW_RIGHT_PRIMARY: 'icon-arrow-right-primary', ARROW_DOWN_PRIMARY: 'icon-arrow-down-primary', @@ -247,6 +252,9 @@ export const Icons = { ICON_UP: 'icon-up', ICON_DOWN: 'icon-down', PAPER_PLANE: 'icon-paper-plane', + PAPER_PLANE_PRIMARY: 'icon-paper-plane-primary', + COMMENT: 'icon-comment', + COMMENT_PLUS: 'icon-comment-plus', }; const SVGIcons: FunctionComponent = ({ @@ -694,6 +702,10 @@ const SVGIcons: FunctionComponent = ({ case Icons.ANNOUNCEMENT: IconComponent = IconAnnouncement; + break; + case Icons.REQUEST: + IconComponent = IconRequest; + break; case Icons.ANNOUNCEMENT_WHITE: IconComponent = IconAnnouncementWhite; @@ -714,6 +726,18 @@ const SVGIcons: FunctionComponent = ({ case Icons.PAPER_PLANE: IconComponent = IconPaperPlane; + break; + case Icons.PAPER_PLANE_PRIMARY: + IconComponent = IconPaperPlanePrimary; + + break; + case Icons.COMMENT: + IconComponent = IconComments; + + break; + case Icons.COMMENT_PLUS: + IconComponent = IconCommentPlus; + break; default: