feat: add tasks tab for feeds widget (#11894)

* feat: add tasks tab for feeds widget

* fix: code smells

* fix: styling issues for tasks tab

* fix: cypress tests
This commit is contained in:
karanh37 2023-06-07 11:28:06 +05:30 committed by GitHub
parent 2b2602b76b
commit c6fdbc43bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 632 additions and 307 deletions

View File

@ -0,0 +1,35 @@
<svg viewBox="0 0 83 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="11" width="68" height="84" rx="5" fill="#292929" fill-opacity="0.05"/>
<rect x="1" y="12" width="66" height="82" rx="4" stroke="black" stroke-opacity="0.5" stroke-width="2"/>
<path d="M10 25H17.5H57C56.8333 17.3333 52 2 34 2C16 2 10.5 17.3333 10 25Z" fill="white"/>
<path d="M9.00212 24.9349C8.98412 25.2109 9.08119 25.4819 9.27025 25.6837C9.45931 25.8855 9.72348 26 10 26H17.5H57C57.269 26 57.5266 25.8916 57.7147 25.6994C57.9029 25.5071 58.0056 25.2472 57.9998 24.9783C57.913 20.9887 56.6206 15.0285 52.9967 10.0375C49.3376 4.99792 43.3339 1 34 1C24.6691 1 18.5089 4.99421 14.645 10.0151C10.8134 14.9941 9.26232 20.9452 9.00212 24.9349Z" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M37 21.5L48.5 12" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 19.5C19 19.5 23 10 33.5 10C44 10 48 19.5 48 19.5" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="33" cy="25" r="6" fill="#D9D9D9"/>
<circle cx="33" cy="25" r="7" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="14" cy="44" r="6" fill="white"/>
<circle cx="14" cy="44" r="7" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="14" cy="65" r="6" fill="white"/>
<circle cx="14" cy="65" r="7" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1 81H44" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 88H14.5" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 88L44 88" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 41H60.5" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 47H49" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M52.5 47H60.5" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 62H60.5" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 67.5H50" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M52.5 67.5H60.5" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M44 93.5V57C44 54.2386 46.2386 52 49 52H69.4289C70.755 52 72.0268 52.5268 72.9645 53.4645L79.0355 59.5355C79.9732 60.4732 80.5 61.745 80.5 63.0711V93.5C80.5 96.2614 78.2614 98.5 75.5 98.5H49C46.2386 98.5 44 96.2614 44 93.5Z" fill="white"/>
<path d="M43 57V93.5C43 96.8137 45.6863 99.5 49 99.5H75.5C78.8137 99.5 81.5 96.8137 81.5 93.5V63.0711C81.5 61.4798 80.8679 59.9536 79.7426 58.8284L73.6716 52.7574C72.5464 51.6321 71.0202 51 69.4289 51H49C45.6863 51 43 53.6863 43 57Z" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M49 65H74" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M49 77H74" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M49 83H55" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M49 71H64" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M68 71H74" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M59 83L74 83" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M74.002 89H63.002" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M59 89H49" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 43.5L12.5 46L16.5 42" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 64.5L12.5 67L16.5 63" stroke="black" stroke-opacity="0.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 12 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.00439453 6.15452C0.00439453 2.85003 2.68325 0.171143 5.98777 0.171143C9.29233 0.171143 11.9712 2.85003 11.9712 6.15452C11.9712 9.45911 9.29233 12.1379 5.98777 12.1379C2.68325 12.1379 0.00439453 9.45911 0.00439453 6.15452ZM5.98777 1.09169C3.19164 1.09169 0.924904 3.35839 0.924904 6.15452C0.924904 8.95069 3.19161 11.2174 5.98777 11.2174C8.78394 11.2174 11.0506 8.95069 11.0506 6.15452C11.0506 3.35839 8.78394 1.09169 5.98777 1.09169ZM5.98777 7.53532C6.75036 7.53532 7.36857 6.9171 7.36857 6.15452C7.36857 5.39194 6.75036 4.77376 5.98777 4.77376C5.22519 4.77376 4.60701 5.39194 4.60701 6.15452C4.60701 6.9171 5.22519 7.53532 5.98777 7.53532Z" fill="#7147E8"/>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@ -14,9 +14,11 @@ import { Col, Row } from 'antd';
import classNames from 'classnames';
import UserPopOverCard from 'components/common/PopOverCard/UserPopOverCard';
import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture';
import Reactions from 'components/Reactions/Reactions';
import { ReactionOperation } from 'enums/reactions.enum';
import { compare } from 'fast-json-patch';
import { Post, ReactionType, Thread } from 'generated/entity/feed/thread';
import { noop } from 'lodash';
import React, { useState } from 'react';
import { useActivityFeedProvider } from '../ActivityFeedProvider/ActivityFeedProvider';
import ActivityFeedActions from '../Shared/ActivityFeedActions';
@ -81,6 +83,7 @@ const ActivityFeedCardV1 = ({
<FeedCardHeaderV1
about={!isPost ? feed.about : undefined}
createdBy={post.from}
isEntityFeed={isPost}
timeStamp={post.postTs}
/>
</Col>
@ -91,9 +94,7 @@ const ActivityFeedCardV1 = ({
announcement={!isPost ? feed.announcement : undefined}
isEditPost={isEditPost}
message={post.message}
reactions={post.reactions}
onEditCancel={() => setIsEditPost(false)}
onReactionUpdate={onReactionUpdate}
onUpdate={onUpdate}
/>
</Col>
@ -101,15 +102,9 @@ const ActivityFeedCardV1 = ({
{!showThread && !isPost && postLength > 0 && (
<Row>
<Col className="p-t-sm" span={24}>
<div className="d-flex items-center">
<div
className="d-flex items-center thread-count cursor-pointer"
onClick={showReplies}>
<ThreadIcon width={18} />{' '}
<span className="text-xs p-l-xss">{postLength}</span>
</div>
<div className="p-l-sm thread-users-profile-pic">
<Col className="p-t-xs" span={24}>
<div className="d-flex items-center gap-2 pl-8">
<div className="thread-users-profile-pic">
{repliedUniqueUsersList.map((user) => (
<UserPopOverCard key={user} userName={user}>
<span
@ -125,6 +120,19 @@ const ActivityFeedCardV1 = ({
</UserPopOverCard>
))}
</div>
<div
className="d-flex items-center thread-count cursor-pointer"
onClick={showReplies}>
<ThreadIcon width={20} />{' '}
<span className="text-xs p-l-xss">{postLength}</span>
</div>
{Boolean(post.reactions?.length) && (
<Reactions
reactions={post.reactions ?? []}
onReactionSelect={onReactionUpdate ?? noop}
/>
)}
</div>
</Col>
</Row>

View File

@ -10,25 +10,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ReactionOperation } from 'enums/reactions.enum';
import {
AnnouncementDetails,
Reaction,
ReactionType,
} from 'generated/entity/feed/thread';
import { AnnouncementDetails } from 'generated/entity/feed/thread';
export interface FeedCardBodyV1Props {
isEditPost: boolean;
className?: string;
showSchedule?: boolean;
showReactions?: boolean;
announcement?: AnnouncementDetails;
message: string;
reactions?: Reaction[];
isOpenInDrawer?: boolean;
onUpdate?: (message: string) => void;
onEditCancel?: () => void;
onReactionUpdate?: (
reaction: ReactionType,
operation: ReactionOperation
) => void;
}

View File

@ -14,8 +14,7 @@ import { Button, Col, Row, Typography } from 'antd';
import classNames from 'classnames';
import ActivityFeedEditor from 'components/ActivityFeed/ActivityFeedEditor/ActivityFeedEditor';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import Reactions from 'components/Reactions/Reactions';
import { isUndefined, noop } from 'lodash';
import { isUndefined } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getFrontEndFormat, MarkdownToHTMLConverter } from 'utils/FeedUtils';
@ -26,13 +25,10 @@ const FeedCardBodyV1 = ({
isEditPost,
className,
showSchedule = true,
showReactions = true,
message,
announcement,
reactions = [],
onUpdate,
onEditCancel,
onReactionUpdate,
}: FeedCardBodyV1Props) => {
const { t } = useTranslation();
const [postMessage, setPostMessage] = useState<string>(message);
@ -122,12 +118,6 @@ const FeedCardBodyV1 = ({
feedBody
)}
</div>
{showReactions && Boolean(reactions?.length) && (
<Reactions
reactions={reactions ?? []}
onReactionSelect={onReactionUpdate ?? noop}
/>
)}
</div>
);
};

View File

@ -19,15 +19,6 @@
a {
color: @link-color !important;
}
.feed-header-timestamp {
font-size: 12px;
color: @grey-2;
}
.feed-header-timestamp::before {
content: '\2022';
margin-right: 8px;
margin-left: 4px;
}
.thread-author {
font-weight: 600;
}
@ -46,3 +37,13 @@
flex-wrap: wrap;
}
}
.feed-header-timestamp {
font-size: 12px;
color: @grey-2;
}
.feed-header-timestamp::before {
content: '\2022';
margin-right: 8px;
margin-left: 4px;
}

View File

@ -58,15 +58,6 @@
border-right: none;
}
.feed-actions:hover {
width: 130px;
}
.feed-actions:hover .expand-button {
opacity: 0;
pointer-events: none;
}
.action-buttons {
position: absolute;
top: 0;
@ -78,11 +69,6 @@
transition: opacity 0.3s, transform 0.3s;
}
.feed-actions:hover .action-buttons {
opacity: 1;
transform: translateX(0);
}
.expand-button {
background-color: transparent;
border: none;
@ -112,3 +98,17 @@
margin-left: -12px;
}
}
.activity-feed-card-v1:hover {
.feed-actions {
width: 130px;
.expand-button {
opacity: 0;
pointer-events: none;
}
.action-buttons {
opacity: 1;
transform: translateX(0);
}
}
}

View File

@ -77,7 +77,11 @@ const ActivityFeedDrawer: FC<ActivityFeedDrawerProps> = ({
<Loader />
) : (
<div id="feed-panel">
<FeedPanelBodyV1 showThread feed={selectedThread as Thread} />
<FeedPanelBodyV1
isOpenInDrawer
showThread
feed={selectedThread as Thread}
/>
<ActivityFeedEditor
buttonClass="tw-mr-4"
className="tw-ml-5 tw-mr-2 tw-mb-2"

View File

@ -12,22 +12,25 @@
*/
import classNames from 'classnames';
import { Post, Thread } from 'generated/entity/feed/thread';
import { Post, Thread, ThreadType } from 'generated/entity/feed/thread';
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { getReplyText } from '../../../utils/FeedUtils';
import ActivityFeedCardV1 from '../ActivityFeedCard/ActivityFeedCardV1';
import TaskFeedCard from '../TaskFeedCard/TaskFeedCard.component';
interface FeedPanelBodyPropV1 {
feed: Thread;
className?: string;
showThread?: boolean;
isOpenInDrawer?: boolean;
}
const FeedPanelBodyV1: FC<FeedPanelBodyPropV1> = ({
feed,
className,
showThread = true,
isOpenInDrawer = false,
}) => {
const { t } = useTranslation();
const mainFeed = {
@ -45,13 +48,24 @@ const FeedPanelBodyV1: FC<FeedPanelBodyPropV1> = ({
'has-replies': showThread && postLength > 0,
})}
data-testid="message-container">
<ActivityFeedCardV1
feed={feed}
isPost={false}
key={feed.id}
post={mainFeed}
showThread={showThread}
/>
{feed.type === ThreadType.Task ? (
<TaskFeedCard
feed={feed}
isOpenInDrawer={isOpenInDrawer}
key={feed.id}
post={mainFeed}
showThread={showThread}
/>
) : (
<ActivityFeedCardV1
feed={feed}
isPost={false}
key={feed.id}
post={mainFeed}
showThread={showThread}
/>
)}
{showThread && postLength > 0 ? (
<div className="feed-posts" data-testid="replies">
<div className="d-flex">

View File

@ -119,6 +119,8 @@ const ActivityFeedActions = ({
const editCheck = useMemo(() => {
if (feed.type === ThreadType.Announcement && !isPost) {
return false;
} else if (feed.type === ThreadType.Task && !isPost) {
return false;
} else if (isAuthor || currentUser?.isAdmin) {
return true;
}

View File

@ -0,0 +1,218 @@
/*
* Copyright 2023 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 { Col, Row, Tooltip, Typography } from 'antd';
import classNames from 'classnames';
import AssigneeList from 'components/common/AssigneeList/AssigneeList';
import EntityPopOverCard from 'components/common/PopOverCard/EntityPopOverCard';
import UserPopOverCard from 'components/common/PopOverCard/UserPopOverCard';
import ProfilePicture from 'components/common/ProfilePicture/ProfilePicture';
import Reactions from 'components/Reactions/Reactions';
import { ReactionOperation } from 'enums/reactions.enum';
import { Post, ReactionType, Thread } from 'generated/entity/feed/thread';
import { isUndefined, toString } from 'lodash';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
entityDisplayName,
getEntityFieldDisplay,
getEntityFQN,
getEntityType,
prepareFeedLink,
} from 'utils/FeedUtils';
import { getTaskDetailPath } from 'utils/TasksUtils';
import {
getDateTimeFromMilliSeconds,
getDayTimeByTimeStamp,
} from 'utils/TimeUtils';
import { useActivityFeedProvider } from '../ActivityFeedProvider/ActivityFeedProvider';
import ActivityFeedActions from '../Shared/ActivityFeedActions';
import './task-feed-card.less';
import { ReactComponent as TaskOpenIcon } from '/assets/svg/ic-open-task.svg';
import { ReactComponent as ThreadIcon } from '/assets/svg/thread.svg';
interface TaskFeedCardProps {
post: Post;
feed: Thread;
className?: string;
showThread?: boolean;
isEntityFeed?: boolean;
isOpenInDrawer?: boolean;
}
const TaskFeedCard = ({
post,
feed,
className = '',
isEntityFeed = false,
showThread = true,
isOpenInDrawer = false,
}: TaskFeedCardProps) => {
const { t } = useTranslation();
const timeStamp = feed.threadTs;
const taskDetails = feed.task;
const postLength = feed?.postsCount ?? 0;
const entityType = getEntityType(feed.about) ?? '';
const entityFQN = getEntityFQN(feed.about) ?? '';
const entityCheck = !isUndefined(entityFQN) && !isUndefined(entityType);
const [isEditPost, setIsEditPost] = useState(false);
const repliedUsers = [...new Set((feed?.posts ?? []).map((f) => f.from))];
const repliedUniqueUsersList = repliedUsers.slice(0, postLength >= 3 ? 2 : 1);
const { showDrawer, updateReactions } = useActivityFeedProvider();
const showReplies = () => {
showDrawer?.(feed);
};
const onEditPost = () => {
setIsEditPost(!isEditPost);
};
const onReactionUpdate = (
reaction: ReactionType,
operation: ReactionOperation
) => {
updateReactions(post, feed.id, true, reaction, operation);
};
const getTaskLinkElement = entityCheck && (
<Typography.Text>
<Link
data-testid="tasklink"
to={getTaskDetailPath(toString(taskDetails?.id)).pathname}
onClick={(e) => e.stopPropagation()}>
<span>{`#${taskDetails?.id} `}</span>
</Link>
<Typography.Text>{taskDetails?.type}</Typography.Text>
<span className="m-x-xss">{t('label.for-lowercase')}</span>
{isEntityFeed ? (
<span className="tw-heading" data-testid="headerText-entityField">
{getEntityFieldDisplay(feed.about)}
</span>
) : (
<>
<span className="p-r-xss">{entityType}</span>
<EntityPopOverCard entityFQN={entityFQN} entityType={entityType}>
<Link
className="break-all"
data-testid="entitylink"
to={prepareFeedLink(entityType, entityFQN)}
onClick={(e) => e.stopPropagation()}>
{entityDisplayName(entityType, entityFQN)}
</Link>
</EntityPopOverCard>
</>
)}
</Typography.Text>
);
return (
<>
<div
className={classNames(
className,
'task-feed-card-v1 activity-feed-card activity-feed-card-v1'
)}>
<Row gutter={[0, 8]}>
<Col span={isOpenInDrawer ? 24 : 18}>
<Row gutter={[0, 8]}>
<Col className="d-flex items-center" span={24}>
<TaskOpenIcon className="m-r-xs" width={14} />
{getTaskLinkElement}
</Col>
<Col span={24}>
<Typography.Text className="task-feed-body text-grey-muted">
<UserPopOverCard
key={feed.createdBy}
userName={feed.createdBy ?? ''}>
<span className="p-r-xss">{feed.createdBy}</span>
</UserPopOverCard>
{t('message.created-this-task-lowercase')}
{timeStamp && (
<Tooltip title={getDateTimeFromMilliSeconds(timeStamp)}>
<span className="p-l-xss" data-testid="timestamp">
{getDayTimeByTimeStamp(timeStamp)}
</span>
</Tooltip>
)}
</Typography.Text>
</Col>
</Row>
</Col>
<Col
className={`d-flex items-center gap-2 ${
!isOpenInDrawer ? 'justify-end' : 'ml-6'
}`}
span={isOpenInDrawer ? 24 : 6}>
<Typography.Text>{t('label.assignee-plural')}</Typography.Text>
<AssigneeList
assignees={feed?.task?.assignees || []}
className="d-flex gap-1"
profilePicType="circle"
profileWidth="24"
showUserName={false}
/>
</Col>
</Row>
{!showThread && postLength > 0 && (
<Row>
<Col className="p-t-xs" span={24}>
<div className="d-flex items-center p-l-lg gap-2">
<div className="thread-users-profile-pic">
{repliedUniqueUsersList.map((user) => (
<UserPopOverCard key={user} userName={user}>
<span
className="profile-image-span cursor-pointer"
data-testid="authorAvatar">
<ProfilePicture
id=""
name={user}
type="circle"
width="24"
/>
</span>
</UserPopOverCard>
))}
</div>
<div
className="d-flex items-center thread-count cursor-pointer"
onClick={showReplies}>
<ThreadIcon width={20} />{' '}
<span className="text-xs p-l-xss">{postLength}</span>
</div>
{Boolean(feed.reactions?.length) && (
<Reactions
reactions={feed.reactions ?? []}
onReactionSelect={onReactionUpdate}
/>
)}
</div>
</Col>
</Row>
)}
<ActivityFeedActions
feed={feed}
isPost={false}
post={post}
onEditPost={onEditPost}
/>
</div>
</>
);
};
export default TaskFeedCard;

View File

@ -0,0 +1,22 @@
/*
* Copyright 2023 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.
*/
.task-feed-card-v1 {
.task-feed-body {
padding-left: 1.5rem;
}
.assignee-item {
margin: 0;
margin-right: 4px;
}
}

View File

@ -12,7 +12,9 @@
*/
import { Card, Col, Row, Typography } from 'antd';
import { ReactComponent as KPIIcon } from 'assets/svg/ic-kpi.svg';
import { AxiosError } from 'axios';
import { DATA_INSIGHT_DOCS } from 'constants/docs.constants';
import { isEmpty, isUndefined } from 'lodash';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -26,6 +28,7 @@ import {
YAxis,
} from 'recharts';
import { getLatestKpiResult, getListKpiResult } from 'rest/KpiAPI';
import { Transi18next } from 'utils/CommonUtils';
import {
getCurrentDateTimeMillis,
getPastDaysDateTimeMillis,
@ -36,7 +39,6 @@ import { Kpi, KpiResult } from '../../generated/dataInsight/kpi/kpi';
import { UIKpiResult } from '../../interface/data-insight.interface';
import { CustomTooltip, getKpiGraphData } from '../../utils/DataInsightUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { EmptyGraphPlaceholder } from './EmptyGraphPlaceholder';
import KPILatestResultsV1 from './KPILatestResultsV1';
interface Props {
@ -44,6 +46,39 @@ interface Props {
selectedDays: number;
}
const EmptyPlaceholder = () => {
const { t } = useTranslation();
return (
<div className="d-flex items-center flex-col p-sm">
<KPIIcon width={100} />
<div className="m-t-xs text-center">
<Typography.Paragraph style={{ marginBottom: '0' }}>
{t('message.adding-new-entity-is-easy-just-give-it-a-spin', {
entity: t('label.data-insight'),
})}
</Typography.Paragraph>
<Typography.Paragraph>
<Transi18next
i18nKey="message.refer-to-our-doc"
renderElement={
<a
href={DATA_INSIGHT_DOCS}
rel="noreferrer"
style={{ color: '#1890ff' }}
target="_blank"
/>
}
values={{
doc: t('label.doc-plural-lowercase'),
}}
/>
</Typography.Paragraph>
</div>
</div>
);
};
const KPIChartV1: FC<Props> = ({ kpiList, selectedDays }) => {
const { t } = useTranslation();
@ -204,12 +239,12 @@ const KPIChartV1: FC<Props> = ({ kpiList, selectedDays }) => {
</>
) : (
<Col className="justify-center" span={24}>
<EmptyGraphPlaceholder />
<EmptyPlaceholder />
</Col>
)}
</Row>
) : (
<EmptyGraphPlaceholder />
<EmptyPlaceholder />
)}
</Card>
);

View File

@ -50,7 +50,7 @@ export const EntityHeader = ({
<div className="w-full">
<div
className={classNames(
'glossary-breadcrumb',
'entity-breadcrumb',
gutter === 'large' ? 'm-b-sm' : 'm-b-xss'
)}
data-testid="category-name">

View File

@ -54,52 +54,56 @@ const RightSidebar = ({
return (
<>
<div className="right-panel-heading p-md p-b-xss">
<Typography.Paragraph className="m-b-sm">
{t('label.recent-announcement-plural')}
</Typography.Paragraph>
<div className="announcement-container-list">
{announcements.map((item) => {
return (
<Alert
className="m-b-xs right-panel-announcement"
description={
<>
<FeedCardHeaderV1
about={item.about}
className="d-inline"
createdBy={item.createdBy}
showUserAvatar={false}
timeStamp={item.threadTs}
/>
<FeedCardBodyV1
announcement={item.announcement}
className="p-t-xs"
isEditPost={false}
message={item.message}
reactions={item.reactions}
showReactions={false}
showSchedule={false}
/>
</>
}
key={item.id}
message={
<div className="d-flex announcement-alert-heading">
<AnnouncementIcon width={20} />
<span className="text-sm p-l-xss">
{t('label.announcement')}
</span>
</div>
}
type="info"
/>
);
})}
</div>
</div>
{announcements.length > 0 && (
<>
<div className="right-panel-heading p-md p-b-xss">
<Typography.Paragraph className="m-b-sm">
{t('label.recent-announcement-plural')}
</Typography.Paragraph>
<div className="announcement-container-list">
{announcements.map((item) => {
return (
<Alert
className="m-b-xs right-panel-announcement"
description={
<>
<FeedCardHeaderV1
about={item.about}
className="d-inline"
createdBy={item.createdBy}
showUserAvatar={false}
timeStamp={item.threadTs}
/>
<FeedCardBodyV1
isOpenInDrawer
announcement={item.announcement}
className="p-t-xs"
isEditPost={false}
message={item.message}
showSchedule={false}
/>
</>
}
key={item.id}
message={
<div className="d-flex announcement-alert-heading">
<AnnouncementIcon width={20} />
<span className="text-sm p-l-xss">
{t('label.announcement')}
</span>
</div>
}
type="info"
/>
);
})}
</div>
</div>
<Divider className="m-0" />
</>
)}
<Divider className="m-0" />
<div className="p-md" data-testid="following-data-container">
<EntityListWithV1
entityList={followedData}

View File

@ -14,4 +14,7 @@
max-height: 360px;
overflow-y: auto;
overflow-x: hidden;
.feed-card-body {
padding: 0;
}
}

View File

@ -103,34 +103,32 @@ const Reactions: FC<ReactionsProps> = ({ reactions, onReactionSelect }) => {
});
return (
<div className="tw-mt-2">
<div className="d-flex">
{emojis}
<Popover
align={{ targetOffset: [0, -10] }}
content={reactionList}
open={visible}
overlayClassName="ant-popover-feed-reactions"
placement="topLeft"
trigger="click"
zIndex={9999}
onOpenChange={handleVisibleChange}>
<Button
className="ant-btn-reaction ant-btn-add-reactions"
data-testid="add-reactions"
shape="round"
onClick={(e) => e.stopPropagation()}>
<SVGIcons
alt="add-reaction"
icon={Icons.ADD_REACTION}
title={t('label.add-entity', {
entity: t('label.reaction-lowercase-plural'),
})}
width="18px"
/>
</Button>
</Popover>
</div>
<div className="d-flex">
{emojis}
<Popover
align={{ targetOffset: [0, -10] }}
content={reactionList}
open={visible}
overlayClassName="ant-popover-feed-reactions"
placement="topLeft"
trigger="click"
zIndex={9999}
onOpenChange={handleVisibleChange}>
<Button
className="ant-btn-reaction ant-btn-add-reactions"
data-testid="add-reactions"
shape="round"
onClick={(e) => e.stopPropagation()}>
<SVGIcons
alt="add-reaction"
icon={Icons.ADD_REACTION}
title={t('label.add-entity', {
entity: t('label.reaction-lowercase-plural'),
})}
width="16px"
/>
</Button>
</Popover>
</div>
);
};

View File

@ -11,25 +11,35 @@
* limitations under the License.
*/
import { Tabs } from 'antd';
import AppState from 'AppState';
import ActivityFeedListV1 from 'components/ActivityFeed/ActivityFeedList/ActivityFeedListV1.component';
import { useActivityFeedProvider } from 'components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
import { FeedFilter } from 'enums/mydata.enum';
import { ThreadType } from 'generated/entity/feed/thread';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getFeedsWithFilter } from 'rest/feedsAPI';
import { showErrorToast } from 'utils/ToastUtils';
import './feeds-widget.less';
const FeedsWidget = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('all');
const { loading, entityThread, getFeedData } = useActivityFeedProvider();
const [taskCount, setTaskCount] = useState(0);
const currentUser = useMemo(
() => AppState.getCurrentUserDetails(),
[AppState.userDetails, AppState.nonSecureUserDetails]
);
useEffect(() => {
if (activeTab === 'all') {
getFeedData(FeedFilter.OWNER).catch(() => {
// ignore since error is displayed in toast in the parent promise.
// Added block for sonar code smell
});
getFeedData(FeedFilter.OWNER, undefined, ThreadType.Conversation).catch(
() => {
// ignore since error is displayed in toast in the parent promise.
// Added block for sonar code smell
}
);
} else if (activeTab === 'mentions') {
getFeedData(FeedFilter.MENTIONS).catch(() => {
// ignore since error is displayed in toast in the parent promise.
@ -43,6 +53,21 @@ const FeedsWidget = () => {
}
}, [activeTab]);
useEffect(() => {
getFeedsWithFilter(
currentUser?.id,
FeedFilter.OWNER,
undefined,
ThreadType.Task
)
.then((res) => {
setTaskCount(res.data.length);
})
.catch((err) => {
showErrorToast(err);
});
}, [currentUser]);
return (
<div className="feeds-widget-container">
<Tabs
@ -70,7 +95,7 @@ const FeedsWidget = () => {
),
},
{
label: t('label.task-plural'),
label: `${t('label.task-plural')} (${taskCount})`,
key: 'tasks',
children: (
<ActivityFeedListV1

View File

@ -40,3 +40,6 @@
border: none;
}
}
.feed-card-body {
padding-left: 32px;
}

View File

@ -12,6 +12,7 @@
*/
import { uniqueId } from 'lodash';
import { ImageShape } from 'Models';
import React, { FC, HTMLAttributes } from 'react';
import { useHistory } from 'react-router-dom';
import { getOwnerValue } from 'utils/CommonUtils';
@ -21,9 +22,18 @@ import ProfilePicture from '../ProfilePicture/ProfilePicture';
interface Props extends HTMLAttributes<HTMLDivElement> {
assignees: EntityReference[];
profilePicType?: ImageShape;
showUserName?: boolean;
profileWidth?: string;
}
const AssigneeList: FC<Props> = ({ assignees, className }) => {
const AssigneeList: FC<Props> = ({
assignees,
className,
profilePicType = 'square',
showUserName = true,
profileWidth = '20',
}) => {
const history = useHistory();
const handleClick = (e: React.MouseEvent, assignee: EntityReference) => {
@ -40,11 +50,18 @@ const AssigneeList: FC<Props> = ({ assignees, className }) => {
type={assignee.type}
userName={assignee.name || ''}>
<span
className="d-flex tw-m-1.5 tw-mt-0 tw-cursor-pointer"
className="assignee-item d-flex m-xss m-t-0 cursor-pointer"
data-testid="assignee"
onClick={(e) => handleClick(e, assignee)}>
<ProfilePicture id="" name={assignee.name || ''} width="20" />
<span className="tw-ml-1">{assignee.name || ''}</span>
<ProfilePicture
id=""
name={assignee.name ?? ''}
type={profilePicType}
width={profileWidth}
/>
{showUserName && (
<span className="m-l-xs">{assignee.name ?? ''}</span>
)}
</span>
</UserPopOverCard>
))}

View File

@ -11,64 +11,66 @@
* limitations under the License.
*/
import { Button, Divider, Popover, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import { Popover } from 'antd';
import { EntityUnion } from 'components/Explore/explore.interface';
import { get, uniqueId } from 'lodash';
import { EntityTags } from 'Models';
import React, { FC, HTMLAttributes, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import ExploreSearchCard from 'components/ExploreV1/ExploreSearchCard/ExploreSearchCard';
import React, {
FC,
HTMLAttributes,
useCallback,
useEffect,
useState,
} from 'react';
import { getDashboardByFqn } from 'rest/dashboardAPI';
import {
getDatabaseDetailsByFQN,
getDatabaseSchemaDetailsByFQN,
} from 'rest/databaseAPI';
import { getGlossaryTermByFQN } from 'rest/glossaryAPI';
import { getMlModelByFQN } from 'rest/mlModelAPI';
import { getPipelineByFqn } from 'rest/pipelineAPI';
import { getTableDetailsByFQN } from 'rest/tableAPI';
import { getTopicByFqn } from 'rest/topicsAPI';
import { getEntityName } from 'utils/EntityUtils';
import AppState from '../../../AppState';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { EntityType } from '../../../enums/entity.enum';
import { Table } from '../../../generated/entity/data/table';
import { TagSource } from '../../../generated/type/tagLabel';
import SVGIcons from '../../../utils/SvgUtils';
import {
getEntityLink,
getTagsWithoutTier,
getTierTags,
} from '../../../utils/TableUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import ProfilePicture from '../ProfilePicture/ProfilePicture';
import RichTextEditorPreviewer from '../rich-text-editor/RichTextEditorPreviewer';
import './popover-card.less';
interface Props extends HTMLAttributes<HTMLDivElement> {
entityType: string;
entityFQN: string;
}
const PopoverContent: React.FC<{
entityData: EntityUnion;
entityFQN: string;
entityType: string;
}> = ({ entityData, entityFQN, entityType }) => {
const name = entityData.name;
const displayName = getEntityName(entityData);
return (
<ExploreSearchCard
id="tabledatacard"
source={{
name,
displayName,
id: entityData.id ?? '',
description: entityData.description ?? '',
fullyQualifiedName: entityFQN,
tags: (entityData as Table).tags,
entityType: entityType,
serviceType: (entityData as Table).serviceType,
}}
/>
);
};
const EntityPopOverCard: FC<Props> = ({ children, entityType, entityFQN }) => {
const { t } = useTranslation();
const [entityData, setEntityData] = useState<EntityUnion>({} as EntityUnion);
const entityTier = useMemo(() => {
const tierFQN = getTierTags((entityData as Table).tags || [])?.tagFQN;
return tierFQN?.split(FQN_SEPARATOR_CHAR)[1];
}, [(entityData as Table).tags]);
const entityTags = useMemo(() => {
const tags: EntityTags[] =
getTagsWithoutTier((entityData as Table).tags || []) || [];
return tags.map((tag) =>
tag.source === TagSource.Glossary ? tag.tagFQN : `#${tag.tagFQN}`
);
}, [(entityData as Table).tags]);
const getData = () => {
const getData = useCallback(() => {
const setEntityDetails = (entityDetail: EntityUnion) => {
AppState.entityData[entityFQN] = entityDetail;
};
@ -105,6 +107,10 @@ const EntityPopOverCard: FC<Props> = ({ children, entityType, entityFQN }) => {
case EntityType.DATABASE_SCHEMA:
promise = getDatabaseSchemaDetailsByFQN(entityFQN, 'owner');
break;
case EntityType.GLOSSARY_TERM:
promise = getGlossaryTermByFQN(entityFQN, 'owner');
break;
default:
@ -118,23 +124,11 @@ const EntityPopOverCard: FC<Props> = ({ children, entityType, entityFQN }) => {
setEntityData(res);
})
.catch((err: AxiosError) => showErrorToast(err));
.catch(() => {
// do nothing
});
}
};
const PopoverTitle = () => {
return (
<Link data-testid="entitylink" to={getEntityLink(entityType, entityFQN)}>
<Button
className="p-0"
disabled={AppState.isTourOpen}
type="link"
onClick={(e) => e.stopPropagation()}>
<span>{entityFQN}</span>
</Button>
</Link>
);
};
}, [entityType, entityFQN]);
const onMouseOver = () => {
const entitydetails = AppState.entityData[entityFQN];
@ -145,95 +139,22 @@ const EntityPopOverCard: FC<Props> = ({ children, entityType, entityFQN }) => {
}
};
const ownerName = useMemo(() => {
return get(entityData, 'owner');
}, [entityData]);
const PopoverContent = () => {
useEffect(() => {
onMouseOver();
}, []);
return (
<div className="w-500">
<Space align="center" size="small">
<div data-testid="owner">
{ownerName ? (
<Space align="center" size="small">
<ProfilePicture
displayName={getEntityName(ownerName)}
id={entityData.name}
name={getEntityName(ownerName)}
width="20"
/>
<Typography.Text className="text-xs">
{getEntityName(ownerName)}
</Typography.Text>
</Space>
) : (
<Typography.Text className="text-xs text-grey-muted">
{t('label.no-entity', {
entity: t('label.owner'),
})}
</Typography.Text>
)}
</div>
<span className="text-grey-muted">|</span>
<Typography.Text
className="text-xs text-grey-muted"
data-testid="tier">
{entityTier
? entityTier
: t('label.no-entity', {
entity: t('label.tier'),
})}
</Typography.Text>
</Space>
<div className="description-text m-t-sm" data-testid="description-text">
{entityData.description ? (
<RichTextEditorPreviewer
enableSeeMoreVariant={false}
markdown={entityData.description}
/>
) : (
<Typography.Text className="text-xs text-grey-muted">
{t('label.no-entity', {
entity: t('label.description'),
})}
</Typography.Text>
)}
</div>
{entityTags.length ? (
<>
<Divider className="m-b-xs m-t-sm" />
<div className="d-flex flex-start">
<span className="w-5 m-r-xs">
<SVGIcons alt="icon-tag" icon="icon-tag-grey" width="14" />
</span>
<Space wrap align="center" size={[16, 0]}>
{entityTags.map((tag) => (
<span className="text-xs font-medium" key={uniqueId()}>
{tag}
</span>
))}
</Space>
</div>
</>
) : null}
</div>
);
};
useEffect(() => {
onMouseOver();
}, [getData, entityFQN]);
return (
<Popover
destroyTooltipOnHide
align={{ targetOffset: [0, -10] }}
content={<PopoverContent />}
overlayClassName="ant-popover-card"
title={<PopoverTitle />}
content={
<PopoverContent
entityData={entityData}
entityFQN={entityFQN}
entityType={entityType}
/>
}
overlayClassName="entity-popover-card"
trigger="hover"
zIndex={9999}>
{children}

View File

@ -68,15 +68,15 @@ const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
const teams = getNonDeletedTeams(userData.teams ?? []);
return teams?.length ? (
<p className="tw-mt-2">
<SVGIcons alt="icon" className="tw-w-4" icon={Icons.TEAMS_GREY} />
<span className="tw-mr-2 tw-ml-1 tw-align-middle tw-font-medium">
<p className="m-t-xs">
<SVGIcons alt="icon" className="w-4" icon={Icons.TEAMS_GREY} />
<span className="m-r-xs m-l-xss align-middle font-medium">
{t('label.team-plural')}
</span>
<span className="d-flex flex-wrap tw-mt-1">
<span className="d-flex flex-wrap m-t-xss">
{teams.map((team, i) => (
<span
className="tw-bg-gray-200 tw-rounded tw-px-1 tw-text-grey-body tw-m-0.5 tw-text-xs"
className="bg-grey rounded-4 p-x-xs text-grey-body text-xs"
key={i}>
{team?.displayName ?? team?.name}
</span>
@ -91,21 +91,19 @@ const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
const isAdmin = userData?.isAdmin;
return roles?.length ? (
<p className="tw-mt-2">
<SVGIcons alt="icon" className="tw-w-4" icon={Icons.USERS} />
<span className="tw-mr-2 tw-ml-1 tw-align-middle tw-font-medium">
<p className="m-t-xs">
<SVGIcons alt="icon" className="w-4" icon={Icons.USERS} />
<span className="m-r-xs m-l-xss align-middle font-medium">
{t('label.role-plural')}
</span>
<span className="d-flex flex-wrap tw-mt-1">
<span className="d-flex flex-wrap m-t-xss">
{isAdmin && (
<span className="tw-bg-gray-200 tw-rounded tw-px-1 tw-text-grey-body tw-m-0.5 tw-text-xs">
<span className="bg-grey rounded-4 p-x-xs text-xs">
{TERM_ADMIN}
</span>
)}
{roles.map((role, i) => (
<span
className="tw-bg-gray-200 tw-rounded tw-px-1 tw-text-grey-body tw-m-0.5 tw-text-xs"
key={i}>
<span className="bg-grey rounded-4 p-x-xs text-xs" key={i}>
{role?.displayName ?? role?.name}
</span>
))}
@ -120,17 +118,17 @@ const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
return (
<div className="d-flex">
<div className="tw-mr-2">
<div className="m-r-xs">
<ProfilePicture id="" name={userName} width="24" />
</div>
<div className="tw-self-center">
<div className="self-center">
<button
className="tw-text-info"
className="text-info"
onClick={(e) => {
e.stopPropagation();
onTitleClickHandler(getUserPath(name));
}}>
<span className="tw-font-medium tw-mr-2">{displayName}</span>
<span className="font-medium m-r-xs">{displayName}</span>
</button>
{displayName !== name ? (
<span className="text-grey-muted">{name}</span>
@ -151,7 +149,7 @@ const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
{isLoading ? (
<Loader size="small" />
) : (
<div className="tw-w-80">
<div className="w-40">
{isEmpty(userData) ? (
<span>{t('message.no-data-available')}</span>
) : (
@ -168,7 +166,6 @@ const UserPopOverCard: FC<Props> = ({ children, userName, type = 'user' }) => {
return (
<Popover
destroyTooltipOnHide
align={{ targetOffset: [0, -10] }}
content={<PopoverContent />}
overlayClassName="ant-popover-card"

View File

@ -0,0 +1,22 @@
/*
* Copyright 2023 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.
*/
.entity-popover-card {
max-width: 500px;
min-width: 300px;
.explore-search-card {
padding: 0;
}
.entity-breadcrumb {
display: none;
}
}

View File

@ -24,6 +24,9 @@
.m-0 {
margin: 0 !important;
}
.m-xss {
margin: @margin-xss;
}
.m-xs {
margin: @margin-xs;
}
@ -321,6 +324,10 @@
padding-left: 0px;
}
.pl-8 {
padding-left: 2rem;
}
.pt-8 {
padding-top: 2rem;
}
@ -413,6 +420,9 @@
.p-r-0 {
padding-right: 0 !important;
}
.p-r-xss {
padding-right: @padding-xss;
}
.p-r-xs {
padding-right: @padding-xs;
}

View File

@ -984,6 +984,9 @@ export const getEntityBreadcrumbs = (
case EntityType.GLOSSARY_TERM:
// eslint-disable-next-line no-case-declarations
const glossary = (entity as GlossaryTerm).glossary;
if (!glossary) {
return [];
}
// eslint-disable-next-line no-case-declarations
const fqnList = Fqn.split((entity as GlossaryTerm).fullyQualifiedName);
// eslint-disable-next-line no-case-declarations