diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx index acf78d51024..8bccef55584 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx @@ -68,6 +68,7 @@ export const ActivityFeedTab = ({ owner, columns, entityType, + isForFeedTab = true, onUpdateEntityDetails, }: ActivityFeedTabProps) => { const history = useHistory(); @@ -379,10 +380,10 @@ export const ActivityFeedTab = ({ )} @@ -427,6 +428,7 @@ export const ActivityFeedTab = ({ void; onUpdateEntityDetails?: () => void; owner?: EntityReference; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCard.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCard.component.test.tsx new file mode 100644 index 00000000000..c70b9b439c7 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCard.component.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { act, render, screen } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { TASK_FEED, TASK_POST } from '../../../mocks/Task.mock'; +import TaskFeedCard from './TaskFeedCard.component'; + +jest.mock('react-router-dom', () => ({ + Link: jest + .fn() + .mockImplementation(({ children }: { children: React.ReactNode }) => ( +

{children}

+ )), + useHistory: jest.fn(), +})); + +jest.mock('../ActivityFeedProvider/ActivityFeedProvider', () => ({ + useActivityFeedProvider: jest.fn().mockImplementation(() => ({ + showDrawer: jest.fn(), + setActiveThread: jest.fn(), + })), + __esModule: true, + default: 'ActivityFeedProvider', +})); + +jest.mock('../../../components/common/AssigneeList/AssigneeList', () => { + return jest.fn().mockImplementation(() =>

AssigneeList

); +}); + +jest.mock('../../../components/common/PopOverCard/EntityPopOverCard', () => { + return jest.fn().mockImplementation(() =>

EntityPopOverCard

); +}); + +jest.mock('../../../components/common/PopOverCard/UserPopOverCard', () => { + return jest.fn().mockImplementation(() =>

UserPopOverCard

); +}); + +jest.mock('../../../components/common/ProfilePicture/ProfilePicture', () => { + return jest.fn().mockImplementation(() =>

ProfilePicture

); +}); + +jest.mock('../Shared/ActivityFeedActions', () => { + return jest.fn().mockImplementation(() =>

ActivityFeedActions

); +}); + +jest.mock('../../../utils/TasksUtils', () => ({ + getTaskDetailPath: jest.fn().mockReturnValue('/'), +})); + +jest.mock('../../../utils/TableUtils', () => ({ + getEntityLink: jest.fn().mockReturnValue('/'), +})); + +jest.mock('../../../utils/FeedUtils', () => ({ + getEntityFQN: jest.fn().mockReturnValue('entityFQN'), + getEntityType: jest.fn().mockReturnValue('entityType'), +})); + +jest.mock('../../../utils/date-time/DateTimeUtils', () => ({ + formatDateTime: jest.fn().mockReturnValue('formatDateTime'), + getRelativeTime: jest.fn().mockReturnValue('getRelativeTime'), +})); + +jest.mock('../../../utils/CommonUtils', () => ({ + getNameFromFQN: jest.fn().mockReturnValue('formatDateTime'), +})); + +const mockProps = { + post: TASK_POST, + feed: TASK_FEED, + showThread: false, + isActive: true, + hidePopover: true, + isForFeedTab: true, +}; + +describe('Test TaskFeedCard Component', () => { + it('Should render TaskFeedCard component', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + expect(screen.getByTestId('task-feed-card')).toBeInTheDocument(); + expect(screen.getByTestId('task-status-icon-open')).toBeInTheDocument(); + expect(screen.getByTestId('redirect-task-button-link')).toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCard.component.tsx index d244e3d7e38..704e7ac512a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCard.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/TaskFeedCard/TaskFeedCard.component.tsx @@ -13,7 +13,7 @@ import Icon from '@ant-design/icons'; import { Button, Col, Row, Tooltip, Typography } from 'antd'; import classNames from 'classnames'; -import { isEmpty, isUndefined, noop } from 'lodash'; +import { isEmpty, isUndefined, lowerCase, noop } from 'lodash'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useHistory } from 'react-router-dom'; @@ -35,11 +35,8 @@ import { getRelativeTime, } from '../../../utils/date-time/DateTimeUtils'; import EntityLink from '../../../utils/EntityLink'; -import { - getEntityFQN, - getEntityType, - prepareFeedLink, -} from '../../../utils/FeedUtils'; +import { getEntityFQN, getEntityType } from '../../../utils/FeedUtils'; +import { getEntityLink } from '../../../utils/TableUtils'; import { getTaskDetailPath } from '../../../utils/TasksUtils'; import { useActivityFeedProvider } from '../ActivityFeedProvider/ActivityFeedProvider'; import ActivityFeedActions from '../Shared/ActivityFeedActions'; @@ -110,8 +107,11 @@ const TaskFeedCard = ({ + onClick={handleTaskLinkClick}> + {`#${taskDetails?.id} `} + {taskDetails?.type} {t('label.for-lowercase')} @@ -122,8 +122,8 @@ const TaskFeedCard = ({ e.stopPropagation()}> {getNameFromFQN(entityFQN)} @@ -140,104 +140,106 @@ const TaskFeedCard = ({ ); return ( - <> -
- - - - - {getTaskLinkElement} - - - - - {feed.createdBy} - - {t('message.created-this-task-lowercase')} - {timeStamp && ( - - - {getRelativeTime(timeStamp)} - - - )} - - - {!showThread ? ( - -
- {postLength > 0 && ( - <> -
- {repliedUniqueUsersList.map((user) => ( - - - - - - ))} -
-
- {' '} - {postLength} -
- - )} - - 0 - ? 'm-l-sm text-sm text-grey-muted' - : 'text-sm text-grey-muted' - }> - {`${t('label.assignee-plural')}: `} - - -
- - ) : null} -
- - {!hidePopover && ( - + + + - )} -
- + + {getTaskLinkElement} + + + + + + {feed.createdBy} + + + {t('message.created-this-task-lowercase')} + {timeStamp && ( + + + {getRelativeTime(timeStamp)} + + + )} + + + {!showThread ? ( + +
+ {postLength > 0 && ( + <> +
+ {repliedUniqueUsersList.map((user) => ( + + + + + + ))} +
+
+ {' '} + {postLength} +
+ + )} + + 0 + ? 'm-l-sm text-sm text-grey-muted' + : 'text-sm text-grey-muted' + }> + {`${t('label.assignee-plural')}: `} + + +
+ + ) : null} + + + {!hidePopover && ( + + )} + ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.test.tsx new file mode 100644 index 00000000000..142fa973c54 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.test.tsx @@ -0,0 +1,161 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { EntityType } from '../../../enums/entity.enum'; +import { TASK_COLUMNS, TASK_FEED } from '../../../mocks/Task.mock'; +import { mockUserData } from '../../Users/mocks/User.mocks'; +import { TaskTab } from './TaskTab.component'; +import { TaskTabProps } from './TaskTab.interface'; + +jest.mock('../../../rest/feedsAPI', () => ({ + updateTask: jest.fn().mockImplementation(() => Promise.resolve()), + updateThread: jest.fn().mockImplementation(() => Promise.resolve()), +})); + +jest.mock('react-router-dom', () => ({ + Link: jest + .fn() + .mockImplementation(({ children }: { children: React.ReactNode }) => ( +

{children}

+ )), + useHistory: jest.fn(), +})); + +jest.mock( + '../../../components/ActivityFeed/ActivityFeedCard/ActivityFeedCardV1', + () => { + return jest.fn().mockImplementation(() =>

ActivityFeedCardV1

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

ActivityFeedEditor

); + } +); + +jest.mock('../../../components/common/AssigneeList/AssigneeList', () => { + return jest.fn().mockImplementation(() =>

AssigneeList

); +}); + +jest.mock('../../../components/common/OwnerLabel/OwnerLabel.component', () => ({ + OwnerLabel: jest.fn().mockImplementation(() =>

OwnerLabel

), +})); + +jest.mock('../../../components/InlineEdit/InlineEdit.component', () => { + return jest.fn().mockImplementation(() =>

InlineEdit

); +}); + +jest.mock('../../../pages/TasksPage/shared/Assignees', () => { + return jest.fn().mockImplementation(() =>

Assignees

); +}); + +jest.mock('../../../pages/TasksPage/shared/DescriptionTask', () => { + return jest.fn().mockImplementation(() =>

DescriptionTask

); +}); + +jest.mock('../../../pages/TasksPage/shared/TagsTask', () => { + return jest.fn().mockImplementation(() =>

TagsTask

); +}); + +jest.mock('../../common/PopOverCard/EntityPopOverCard', () => { + return jest.fn().mockImplementation(() =>

EntityPopOverCard

); +}); + +jest.mock('../../../utils/CommonUtils', () => ({ + getNameFromFQN: jest.fn().mockReturnValue('getNameFromFQN'), +})); + +jest.mock('../../../utils/EntityUtils', () => ({ + getEntityName: jest.fn().mockReturnValue('getEntityName'), +})); + +jest.mock('../../../utils/FeedUtils', () => ({ + getEntityFQN: jest.fn().mockReturnValue('getEntityFQN'), +})); + +jest.mock('../../../utils/TableUtils', () => ({ + getEntityLink: jest.fn().mockReturnValue('getEntityLink'), +})); + +jest.mock('../../../utils/TasksUtils', () => ({ + fetchOptions: jest.fn().mockReturnValue('getEntityLink'), + getTaskDetailPath: jest.fn().mockReturnValue('/'), + isDescriptionTask: jest.fn().mockReturnValue(false), + isTagsTask: jest.fn().mockReturnValue(true), + TASK_ACTION_LIST: jest.fn().mockReturnValue([]), +})); + +jest.mock('../../../utils/ToastUtils', () => ({ + showErrorToast: jest.fn(), + showSuccessToast: jest.fn(), +})); + +jest.mock('../../Auth/AuthProviders/AuthProvider', () => ({ + useAuthContext: jest.fn(() => ({ + currentUser: mockUserData, + })), +})); + +jest.mock('../../../rest/feedsAPI', () => ({ + updateTask: jest.fn(), + updateThread: jest.fn(), +})); + +jest.mock( + '../../../components/ActivityFeed/ActivityFeedProvider/ActivityFeedProvider', + () => ({ + useActivityFeedProvider: jest.fn().mockImplementation(() => ({ + postFeed: jest.fn(), + setActiveThread: jest.fn(), + })), + __esModule: true, + default: 'ActivityFeedProvider', + }) +); + +jest.mock('../../../hooks/authHooks', () => ({ + useAuth: () => { + return { + isAdminUser: false, + }; + }, +})); + +const mockOnAfterClose = jest.fn(); +const mockOnUpdateEntityDetails = jest.fn(); + +const mockProps: TaskTabProps = { + taskThread: TASK_FEED, + entityType: EntityType.TABLE, + isForFeedTab: true, + columns: TASK_COLUMNS, + onAfterClose: mockOnAfterClose, + onUpdateEntityDetails: mockOnUpdateEntityDetails, +}; + +describe('Test TaskFeedCard component', () => { + it('Should render the component', async () => { + render(, { + wrapper: MemoryRouter, + }); + + const activityFeedCard = screen.getByTestId('task-tab'); + + expect(activityFeedCard).toBeInTheDocument(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.tsx index 13b9771b19e..b0fc071c340 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Task/TaskTab/TaskTab.component.tsx @@ -31,6 +31,7 @@ import { isEmpty, isEqual, isUndefined, noop } from 'lodash'; import { MenuInfo } from 'rc-menu/lib/interface'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Link, useHistory } from 'react-router-dom'; import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg'; import { ReactComponent as TaskCloseIcon } from '../../../assets/svg/ic-close-task.svg'; import { ReactComponent as TaskOpenIcon } from '../../../assets/svg/ic-open-task.svg'; @@ -58,17 +59,21 @@ import { TaskActionMode, } from '../../../pages/TasksPage/TasksPage.interface'; import { updateTask, updateThread } from '../../../rest/feedsAPI'; +import { getNameFromFQN } from '../../../utils/CommonUtils'; import EntityLink from '../../../utils/EntityLink'; import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityFQN } from '../../../utils/FeedUtils'; +import { getEntityLink } from '../../../utils/TableUtils'; import { fetchOptions, + getTaskDetailPath, isDescriptionTask, isTagsTask, TASK_ACTION_LIST, } from '../../../utils/TasksUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { useAuthContext } from '../../Auth/AuthProviders/AuthProvider'; +import EntityPopOverCard from '../../common/PopOverCard/EntityPopOverCard'; import './task-tab.less'; import { TaskTabProps } from './TaskTab.interface'; @@ -76,8 +81,10 @@ export const TaskTab = ({ taskThread, owner, entityType, + isForFeedTab, ...rest }: TaskTabProps) => { + const history = useHistory(); const [assigneesForm] = useForm(); const { currentUser } = useAuthContext(); const updatedAssignees = Form.useWatch('assignees', assigneesForm); @@ -142,14 +149,44 @@ export const TaskTab = ({ const isTaskGlossaryApproval = taskDetails?.type === TaskType.RequestApproval; + const handleTaskLinkClick = () => { + history.push({ + pathname: getTaskDetailPath(taskThread), + }); + }; + const getTaskLinkElement = entityCheck && ( - {`#${taskDetails?.id} `} + {taskDetails?.type} {t('label.for-lowercase')} - {!isEmpty(taskField) ? {taskField} : null} + {!isForFeedTab && ( + <> + {entityType} + + e.stopPropagation()}> + + {' '} + {getNameFromFQN(entityFQN)} + + + + + )} + {!isEmpty(taskField) ? ( + {taskField} + ) : null} ); @@ -418,7 +455,7 @@ export const TaskTab = ({ }, [initialAssignees]); return ( - + void; onAfterClose?: () => void; } & ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx index 57a73c26708..925340ec2ec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Users/Users.component.tsx @@ -159,6 +159,7 @@ const Users = ({ diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts index 6f0f53c9743..17bdfcf6c9c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/regex.constants.ts @@ -46,3 +46,5 @@ export const ENDS_WITH_NUMBER_REGEX = /\d+$/; export const VALID_OBJECT_KEY_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/; export const HEX_COLOR_CODE_REGEX = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; + +export const TASK_SANITIZE_VALUE_REGEX = /^"|"$/g; diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/TableVersion.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/TableVersion.mock.ts index 0456b3328b0..b31623ac3a7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/mocks/TableVersion.mock.ts +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/TableVersion.mock.ts @@ -12,7 +12,13 @@ */ import { TableVersionProp } from '../components/TableVersion/TableVersion.interface'; -import { DatabaseServiceType, TableType } from '../generated/entity/data/table'; +import { + Constraint, + DatabaseServiceType, + DataType, + Table, + TableType, +} from '../generated/entity/data/table'; import { ENTITY_PERMISSIONS } from '../mocks/Permissions.mock'; import { mockBackHandler, @@ -23,7 +29,7 @@ import { mockVersionList, } from '../mocks/VersionCommon.mock'; -export const mockTableData = { +export const mockTableData: Table = { id: 'ab4f893b-c303-43d9-9375-3e620a670b02', name: 'raw_product_catalog', fullyQualifiedName: 'sample_data.ecommerce_db.shopify.raw_product_catalog', @@ -33,7 +39,20 @@ export const mockTableData = { updatedAt: 1688442727895, updatedBy: 'admin', tableType: TableType.Regular, - columns: [], + columns: [ + { + name: 'shop_id', + displayName: 'Shop Id Customer', + dataType: DataType.Number, + dataTypeDisplay: 'numeric', + description: + 'Unique identifier for the store. This column is the primary key for this table.', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify."dim.shop".shop_id', + tags: [], + constraint: Constraint.PrimaryKey, + ordinalPosition: 1, + }, + ], owner: { id: '38be030f-f817-4712-bc3b-ff7b9b9b805e', type: 'user', diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/Task.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/Task.mock.ts new file mode 100644 index 00000000000..21c46160af5 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/Task.mock.ts @@ -0,0 +1,103 @@ +/* + * 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 { + Column, + Constraint, + DataType, +} from '../generated/entity/data/container'; +import { + Post, + TaskType, + Thread, + ThreadTaskStatus, + ThreadType, +} from '../generated/entity/feed/thread'; + +/* eslint-disable max-len */ +export const TASK_FEED: Thread = { + id: '8b5076bb-8284-46b0-b00d-5e43a184ba9b', + type: ThreadType.Task, + href: 'http://localhost:8585/api/v1/feed/8b5076bb-8284-46b0-b00d-5e43a184ba9b', + threadTs: 1701686127533, + about: + '<#E::table::sample_data.ecommerce_db.shopify."dim.shop"::columns::shop_id::tags>', + entityId: 'defcff8c-0823-40e6-9c1e-9b0458ba0fa5', + createdBy: 'admin', + updatedAt: 1701686127534, + updatedBy: 'admin', + resolved: false, + message: 'Request tags for table dim.shop columns/shop_id', + postsCount: 0, + posts: [], + reactions: [], + task: { + id: 2, + type: TaskType.RequestTag, + assignees: [ + { + id: '31d072f8-7873-4976-88ea-ac0d2f51f632', + type: 'team', + name: 'Sales', + fullyQualifiedName: 'Sales', + deleted: false, + }, + ], + status: ThreadTaskStatus.Open, + oldValue: '[]', + suggestion: + '[{"tagFQN":"PersonalData.SpecialCategory","source":"Classification","name":"SpecialCategory","description":"GDPR special category data is personal information of data subjects that is especially sensitive, the exposure of which could significantly impact the rights and freedoms of data subjects and potentially be used against them for unlawful discrimination."}]', + }, +}; + +export const TASK_POST: Post = { + message: 'Request tags for table dim.shop columns/shop_id', + postTs: 1701686127533, + from: 'admin', + id: '8b5076bb-8284-46b0-b00d-5e43a184ba9b', + reactions: [], +}; + +export const TASK_COLUMNS: Column[] = [ + { + name: 'shop_id', + dataType: DataType.Number, + dataTypeDisplay: 'numeric', + description: + 'Unique identifier for the store. This column is the primary key for this table.', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify."dim.shop".shop_id', + tags: [], + constraint: Constraint.PrimaryKey, + ordinalPosition: 1, + }, + { + name: 'name', + dataType: DataType.Varchar, + dataLength: 100, + dataTypeDisplay: 'varchar', + description: 'Name of your store.', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify."dim.shop".name', + tags: [], + ordinalPosition: 2, + }, + { + name: 'domain', + dataType: DataType.Varchar, + dataLength: 1000, + dataTypeDisplay: 'varchar', + description: + 'Primary domain specified for your online store. Your primary domain is the one that your customers and search engines see. For example, www.mycompany.com.', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify."dim.shop".domain', + tags: [], + ordinalPosition: 3, + }, +]; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestDescriptionPage/RequestDescriptionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestDescriptionPage/RequestDescriptionPage.tsx index c094d275e52..da9db4f71c7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestDescriptionPage/RequestDescriptionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestDescriptionPage/RequestDescriptionPage.tsx @@ -46,6 +46,7 @@ import { fetchEntityDetail, fetchOptions, getBreadCrumbList, + getTaskMessage, } from '../../../utils/TasksUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import Assignees from '../shared/Assignees'; @@ -72,11 +73,17 @@ const RequestDescription = () => { const [assignees, setAssignees] = useState>([]); const [suggestion, setSuggestion] = useState(''); - const getSanitizeValue = value?.replaceAll(/^"|"$/g, '') || ''; - - const message = `Request description for ${getSanitizeValue || entityType} ${ - field !== EntityField.COLUMNS ? getEntityName(entityData) : '' - }`; + const taskMessage = useMemo( + () => + getTaskMessage({ + value, + entityType, + entityData, + field, + startMessage: 'Request description', + }), + [value, entityType, field, entityData] + ); const decodedEntityFQN = useMemo( () => getDecodedFqn(entityFQN), @@ -105,7 +112,7 @@ const RequestDescription = () => { if (assignees.length) { const data: CreateThread = { from: currentUser?.name as string, - message: value.title || message, + message: value.title || taskMessage, about: getEntityFeedLink(entityType, decodedEntityFQN, getTaskAbout()), taskDetails: { assignees: assignees.map((assignee) => ({ @@ -159,7 +166,7 @@ const RequestDescription = () => { setOptions(defaultAssignee); } form.setFieldsValue({ - title: message.trimEnd(), + title: taskMessage.trimEnd(), assignees: defaultAssignee, }); }, [entityData]); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestTagPage/RequestTagPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestTagPage/RequestTagPage.tsx index f7b1f328c44..363e0daf843 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestTagPage/RequestTagPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/RequestTagPage/RequestTagPage.tsx @@ -45,6 +45,7 @@ import { fetchEntityDetail, fetchOptions, getBreadCrumbList, + getTaskMessage, } from '../../../utils/TasksUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import Assignees from '../shared/Assignees'; @@ -70,11 +71,17 @@ const RequestTag = () => { const [assignees, setAssignees] = useState([]); const [suggestion] = useState([]); - const getSanitizeValue = value?.replaceAll(/^"|"$/g, '') || ''; - - const message = `Request tags for ${getSanitizeValue || entityType} ${ - field !== EntityField.COLUMNS ? getEntityName(entityData) : '' - }`; + const taskMessage = useMemo( + () => + getTaskMessage({ + value, + entityType, + entityData, + field, + startMessage: 'Request tags', + }), + [value, entityType, field, entityData] + ); const decodedEntityFQN = useMemo( () => getDecodedFqn(entityFQN), @@ -98,7 +105,7 @@ const RequestTag = () => { const onCreateTask: FormProps['onFinish'] = (value) => { const data: CreateThread = { from: currentUser?.name as string, - message: value.title || message, + message: value.title || taskMessage, about: getEntityFeedLink(entityType, decodedEntityFQN, getTaskAbout()), taskDetails: { assignees: assignees.map((assignee) => ({ @@ -149,7 +156,7 @@ const RequestTag = () => { setOptions((prev) => [...defaultAssignee, ...prev]); } form.setFieldsValue({ - title: message.trimEnd(), + title: taskMessage.trimEnd(), assignees: defaultAssignee, }); }, [entityData]); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateDescriptionPage/UpdateDescriptionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateDescriptionPage/UpdateDescriptionPage.tsx index fbdefca7a30..6f52e871970 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateDescriptionPage/UpdateDescriptionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateDescriptionPage/UpdateDescriptionPage.tsx @@ -27,6 +27,7 @@ import Loader from '../../../components/Loader/Loader'; import { SearchedDataProps } from '../../../components/SearchedData/SearchedData.interface'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { EntityField } from '../../../constants/Feeds.constants'; +import { TASK_SANITIZE_VALUE_REGEX } from '../../../constants/regex.constants'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { CreateThread, @@ -47,6 +48,7 @@ import { getBreadCrumbList, getColumnObject, getEntityColumnsDetails, + getTaskMessage, } from '../../../utils/TasksUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import Assignees from '../shared/Assignees'; @@ -73,18 +75,29 @@ const UpdateDescription = () => { const [assignees, setAssignees] = useState>([]); const [currentDescription, setCurrentDescription] = useState(''); - const getSanitizeValue = value?.replaceAll(/^"|"$/g, '') || ''; + const sanitizeValue = useMemo( + () => value?.replaceAll(TASK_SANITIZE_VALUE_REGEX, '') ?? '', + [value] + ); const decodedEntityFQN = useMemo(() => getDecodedFqn(entityFQN), [entityFQN]); - const message = `Update description for ${getSanitizeValue || entityType} ${ - field !== EntityField.COLUMNS ? getEntityName(entityData) : '' - }`; + const taskMessage = useMemo( + () => + getTaskMessage({ + value, + entityType, + entityData, + field, + startMessage: 'Update description', + }), + [value, entityType, field, entityData] + ); const back = () => history.goBack(); const columnObject = useMemo(() => { - const column = getSanitizeValue.split(FQN_SEPARATOR_CHAR).slice(-1); + const column = sanitizeValue.split(FQN_SEPARATOR_CHAR).slice(-1); return getColumnObject( column[0], @@ -116,7 +129,7 @@ const UpdateDescription = () => { const onCreateTask: FormProps['onFinish'] = (value) => { const data: CreateThread = { from: currentUser?.name as string, - message: value.title || message, + message: value.title || taskMessage, about: getEntityFeedLink(entityType, decodedEntityFQN, getTaskAbout()), taskDetails: { assignees: assignees.map((assignee) => ({ @@ -167,7 +180,7 @@ const UpdateDescription = () => { setOptions(defaultAssignee); } form.setFieldsValue({ - title: message.trimEnd(), + title: taskMessage.trimEnd(), assignees: defaultAssignee, description: getDescription(), }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateTagPage/UpdateTagPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateTagPage/UpdateTagPage.tsx index d785ac6888c..fcbaae57b38 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateTagPage/UpdateTagPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TasksPage/UpdateTagPage/UpdateTagPage.tsx @@ -27,6 +27,7 @@ import Loader from '../../../components/Loader/Loader'; import { SearchedDataProps } from '../../../components/SearchedData/SearchedData.interface'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; import { EntityField } from '../../../constants/Feeds.constants'; +import { TASK_SANITIZE_VALUE_REGEX } from '../../../constants/regex.constants'; import { EntityTabs, EntityType } from '../../../enums/entity.enum'; import { CreateThread, @@ -49,6 +50,7 @@ import { getBreadCrumbList, getColumnObject, getEntityColumnsDetails, + getTaskMessage, } from '../../../utils/TasksUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import Assignees from '../shared/Assignees'; @@ -78,17 +80,29 @@ const UpdateTag = () => { const [currentTags, setCurrentTags] = useState([]); const [suggestion, setSuggestion] = useState([]); - const getSanitizeValue = value?.replaceAll(/^"|"$/g, '') || ''; + const sanitizeValue = useMemo( + () => value?.replaceAll(TASK_SANITIZE_VALUE_REGEX, '') ?? '', + [value] + ); + + const taskMessage = useMemo( + () => + getTaskMessage({ + value, + entityType, + entityData, + field, + startMessage: 'Update tags', + }), + [value, entityType, field, entityData] + ); - const message = `Update tags for ${getSanitizeValue || entityType} ${ - field !== EntityField.COLUMNS ? getEntityName(entityData) : '' - }`; const decodedEntityFQN = useMemo(() => getDecodedFqn(entityFQN), [entityFQN]); const back = () => history.goBack(); const columnObject = useMemo(() => { - const column = getSanitizeValue.split(FQN_SEPARATOR_CHAR).slice(-1); + const column = sanitizeValue.split(FQN_SEPARATOR_CHAR).slice(-1); return getColumnObject( column[0], @@ -121,7 +135,7 @@ const UpdateTag = () => { const onCreateTask: FormProps['onFinish'] = (value) => { const data: CreateThread = { from: currentUser?.name as string, - message: value.title || message, + message: value.title || taskMessage, about: getEntityFeedLink(entityType, decodedEntityFQN, getTaskAbout()), taskDetails: { assignees: assignees.map((assignee) => ({ @@ -177,7 +191,7 @@ const UpdateTag = () => { setOptions(defaultAssignee); } form.setFieldsValue({ - title: message.trimEnd(), + title: taskMessage.trimEnd(), updatedTags: getTags(), assignees: defaultAssignee, }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.test.tsx new file mode 100644 index 00000000000..bdc725d04e8 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.test.tsx @@ -0,0 +1,159 @@ +/* + * 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 { EntityType } from '../enums/entity.enum'; +import { mockTableData } from '../mocks/TableVersion.mock'; +import { getEntityTableName, getTaskMessage } from './TasksUtils'; + +describe('Tests for DataAssetsHeaderUtils', () => { + it('function getEntityTableName should return name if no data found', () => { + const entityName = getEntityTableName( + EntityType.TABLE, + 'data_test_id', + mockTableData + ); + + expect(entityName).toEqual('data_test_id'); + }); + + it('function getEntityTableName should return name if it contains dot in it name', () => { + const entityName = getEntityTableName( + EntityType.TABLE, + 'data.test_id', + mockTableData + ); + + expect(entityName).toEqual('data.test_id'); + }); + + it('function getEntityTableName should return name if entity type not found', () => { + const entityName = getEntityTableName( + EntityType.DATABASE_SERVICE, + 'cyber_test', + mockTableData + ); + + expect(entityName).toEqual('cyber_test'); + }); + + it('function getEntityTableName should return entity display name for all entities', () => { + const entityTableName = getEntityTableName( + EntityType.TABLE, + 'shop_id', + mockTableData + ); + + expect(entityTableName).toEqual('Shop Id Customer'); + }); +}); + +const taskTagMessage = { + value: null, + entityType: EntityType.TABLE, + entityData: mockTableData, + field: null, + startMessage: 'Request Tag', +}; + +const taskDescriptionMessage = { + ...taskTagMessage, + startMessage: 'Request Description', +}; + +describe('Tests for getTaskMessage', () => { + it('function getTaskMessage should return task message for tags', () => { + // entity request task message + const requestTagsEntityMessage = getTaskMessage(taskTagMessage); + + expect(requestTagsEntityMessage).toEqual( + 'Request Tag for table raw_product_catalog ' + ); + + // entity request column message + const requestTagsEntityColumnMessage = getTaskMessage({ + ...taskTagMessage, + value: 'order_id', + field: 'columns', + }); + + expect(requestTagsEntityColumnMessage).toEqual( + 'Request Tag for table raw_product_catalog columns/order_id' + ); + + // entity update task message + const updateTagsEntityMessage = getTaskMessage({ + ...taskTagMessage, + startMessage: 'Update Tag', + }); + + expect(updateTagsEntityMessage).toEqual( + 'Update Tag for table raw_product_catalog ' + ); + + // entity update column message + const updateTagsEntityColumnMessage = getTaskMessage({ + ...taskTagMessage, + value: 'order_id', + field: 'columns', + startMessage: 'Update Tag', + }); + + expect(updateTagsEntityColumnMessage).toEqual( + 'Update Tag for table raw_product_catalog columns/order_id' + ); + }); + + it('function getTaskMessage should return task message for description', () => { + // entity request task message + const requestDescriptionEntityMessage = getTaskMessage( + taskDescriptionMessage + ); + + expect(requestDescriptionEntityMessage).toEqual( + 'Request Description for table raw_product_catalog ' + ); + + // entity request column message + const requestDescriptionEntityColumnMessage = getTaskMessage({ + ...taskDescriptionMessage, + value: 'order_id', + field: 'columns', + }); + + expect(requestDescriptionEntityColumnMessage).toEqual( + 'Request Description for table raw_product_catalog columns/order_id' + ); + + // entity update task message + const updateDescriptionEntityMessage = getTaskMessage({ + ...taskDescriptionMessage, + startMessage: 'Update Description', + }); + + expect(updateDescriptionEntityMessage).toEqual( + 'Update Description for table raw_product_catalog ' + ); + + // entity update column message + const updateDescriptionEntityColumnMessage = getTaskMessage({ + ...taskDescriptionMessage, + value: 'order_id', + field: 'columns', + startMessage: 'Update Description', + }); + + expect(updateDescriptionEntityColumnMessage).toEqual( + 'Update Description for table raw_product_catalog columns/order_id' + ); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts index bdc7f1bb4c1..7568ab39dbc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TasksUtils.ts @@ -25,6 +25,7 @@ import { ROUTES, } from '../constants/constants'; import { EntityField } from '../constants/Feeds.constants'; +import { TASK_SANITIZE_VALUE_REGEX } from '../constants/regex.constants'; import { EntityTabs, EntityType, @@ -35,8 +36,10 @@ import { ServiceCategory } from '../enums/service.enum'; import { Chart } from '../generated/entity/data/chart'; import { Container } from '../generated/entity/data/container'; import { Dashboard } from '../generated/entity/data/dashboard'; +import { DashboardDataModel } from '../generated/entity/data/dashboardDataModel'; import { MlFeature, Mlmodel } from '../generated/entity/data/mlmodel'; import { Pipeline, Task } from '../generated/entity/data/pipeline'; +import { SearchIndex } from '../generated/entity/data/searchIndex'; import { Column, Table } from '../generated/entity/data/table'; import { Field, Topic } from '../generated/entity/data/topic'; import { TaskType, Thread } from '../generated/entity/feed/thread'; @@ -590,3 +593,105 @@ export const getEntityTaskDetails = ( return { fqnPart: [fqnPartTypes], entityField }; }; + +export const getEntityTableName = ( + entityType: EntityType, + name: string, + entityData: EntityData +): string => { + if (name.includes('.')) { + return name; + } + let entityReference; + + switch (entityType) { + case EntityType.TABLE: + entityReference = (entityData as Table).columns?.find( + (item) => item.name === name + ); + + break; + + case EntityType.TOPIC: + entityReference = (entityData as Topic).messageSchema?.schemaFields?.find( + (item) => item.name === name + ); + + break; + + case EntityType.DASHBOARD: + entityReference = (entityData as Dashboard).charts?.find( + (item) => item.name === name + ); + + break; + + case EntityType.PIPELINE: + entityReference = (entityData as Pipeline).tasks?.find( + (item) => item.name === name + ); + + break; + + case EntityType.MLMODEL: + entityReference = (entityData as Mlmodel).mlFeatures?.find( + (item) => item.name === name + ); + + break; + + case EntityType.CONTAINER: + entityReference = (entityData as Container).dataModel?.columns?.find( + (item) => item.name === name + ); + + break; + + case EntityType.SEARCH_INDEX: + entityReference = (entityData as SearchIndex).fields?.find( + (item) => item.name === name + ); + + break; + + case EntityType.DASHBOARD_DATA_MODEL: + entityReference = (entityData as DashboardDataModel).columns?.find( + (item) => item.name === name + ); + + break; + + default: + return name; + } + + if (isUndefined(entityReference)) { + return name; + } + + return getEntityName(entityReference); +}; + +export const getTaskMessage = ({ + value, + entityType, + entityData, + field, + startMessage, +}: { + value: string | null; + entityType: EntityType; + entityData: EntityData; + field: string | null; + startMessage: string; +}) => { + const sanitizeValue = value?.replaceAll(TASK_SANITIZE_VALUE_REGEX, '') ?? ''; + + const entityColumnsName = field + ? `${field}/${getEntityTableName(entityType, sanitizeValue, entityData)}` + : ''; + + return `${startMessage} for ${entityType} ${getEntityName( + entityData + )} ${entityColumnsName}`; +};