ui: revamp entity task component (#12419)

* revamp entity task component

* added test related to tags and description

---------

Co-authored-by: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com>
This commit is contained in:
Ashish Gupta 2023-07-15 11:23:25 +05:30 committed by GitHub
parent 0a6e91d69a
commit fc29eba285
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 308 additions and 183 deletions

View File

@ -140,7 +140,7 @@ const ContainerDataModel: FC<ContainerDataModelProps> = ({
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
field: record.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}

View File

@ -470,7 +470,7 @@ const DashboardDetails = ({
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
field: record.description,
}}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,

View File

@ -110,7 +110,7 @@ const ModelTab = ({
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
field: record.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}

View File

@ -209,7 +209,7 @@ const MlModelFeaturesList = ({
<TableDescription
columnData={{
fqn: feature.fullyQualifiedName ?? '',
description: feature.description,
field: feature.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}

View File

@ -394,7 +394,7 @@ const PipelineDetails = ({
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
field: record.description,
}}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.TASKS,

View File

@ -258,7 +258,7 @@ const SchemaTable = ({
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
field: record.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}

View File

@ -14,7 +14,8 @@
import { Space } from 'antd';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import EntityTaskDescription from 'pages/TasksPage/EntityTaskDescription/EntityTaskDescription.component';
import { EntityField } from 'constants/Feeds.constants';
import EntityTasks from 'pages/TasksPage/EntityTasks/EntityTasks.component';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../assets/svg/edit-new.svg';
@ -39,8 +40,8 @@ const TableDescription = ({
data-testid="description"
direction="vertical"
id={`field-description-${index}`}>
{columnData.description ? (
<RichTextEditorPreviewer markdown={columnData.description} />
{columnData.field ? (
<RichTextEditorPreviewer markdown={columnData.field} />
) : (
<span className="text-grey-muted">
{t('label.no-entity', {
@ -62,10 +63,11 @@ const TableDescription = ({
/>
)}
<EntityTaskDescription
<EntityTasks
data={columnData}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityTaskType={EntityField.DESCRIPTION}
entityType={entityType}
onThreadLinkSelect={onThreadLinkSelect}
/>

View File

@ -19,7 +19,7 @@ export interface TableDescriptionProps {
index: number;
columnData: {
fqn: string;
description?: string;
field?: string;
};
entityFqn: string;
entityType: EntityType;

View File

@ -13,7 +13,8 @@
import classNames from 'classnames';
import TagsContainerV2 from 'components/Tag/TagsContainerV2/TagsContainerV2';
import EntityTaskTags from 'pages/TasksPage/EntityTaskTags/EntityTaskTags.component';
import { EntityField } from 'constants/Feeds.constants';
import EntityTasks from 'pages/TasksPage/EntityTasks/EntityTasks.component';
import React from 'react';
import { TableTagsComponentProps, TableUnion } from './TableTags.interface';
@ -48,13 +49,14 @@ const TableTags = <T extends TableUnion>({
}}>
<>
{!isReadOnly && (
<EntityTaskTags
<EntityTasks
data={{
fqn: record.fullyQualifiedName ?? '',
tags: record.tags ?? [],
field: record.tags ?? [],
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}
entityTaskType={EntityField.TAGS}
entityType={entityType}
tagSource={type}
onThreadLinkSelect={onThreadLinkSelect}

View File

@ -27,8 +27,8 @@ jest.mock('utils/FeedElementUtils', () => ({
),
}));
jest.mock('pages/TasksPage/EntityTaskTags/EntityTaskTags.component', () => {
return jest.fn().mockImplementation(() => <div>EntityTaskTags</div>);
jest.mock('pages/TasksPage/EntityTasks/EntityTasks.component', () => {
return jest.fn().mockImplementation(() => <div>EntityTasks</div>);
});
const glossaryTags = [
@ -171,10 +171,10 @@ describe('Test EntityTableTags Component', () => {
);
const tagContainer = await screen.findByTestId('Classification-tags-0');
const entityTaskTags = screen.queryByText('EntityTaskTags');
const entityTasks = screen.queryByText('EntityTasks');
expect(tagContainer).toBeInTheDocument();
expect(entityTaskTags).not.toBeInTheDocument();
expect(entityTasks).not.toBeInTheDocument();
});
it('Should render update and request tags buttons', async () => {
@ -194,9 +194,9 @@ describe('Test EntityTableTags Component', () => {
);
const tagContainer = await screen.findByTestId('Classification-tags-0');
const entityTaskTags = screen.queryByText('EntityTaskTags');
const entityTasks = screen.queryByText('EntityTasks');
expect(tagContainer).toBeInTheDocument();
expect(entityTaskTags).toBeInTheDocument();
expect(entityTasks).toBeInTheDocument();
});
});

View File

@ -182,7 +182,7 @@ const TopicSchemaFields: FC<TopicSchemaFieldsProps> = ({
<TableDescription
columnData={{
fqn: record.fullyQualifiedName ?? '',
description: record.description,
field: record.description,
}}
entityFieldThreads={entityFieldThreads}
entityFqn={entityFqn}

View File

@ -13,6 +13,7 @@
import { startCase } from 'lodash';
import i18n from 'utils/i18next/LocalUtil';
import { EntityField } from './Feeds.constants';
export const ENTITY_DELETE_STATE = {
loading: 'initial',
@ -32,3 +33,14 @@ export const STEPS_FOR_IMPORT_ENTITY = [
step: 2,
},
];
export const ENTITY_TASKS_TOOLTIP = {
[EntityField.DESCRIPTION]: {
request: i18n.t('message.request-description'),
update: i18n.t('message.request-update-description'),
},
[EntityField.TAGS]: {
request: i18n.t('label.request-tag-plural'),
update: i18n.t('label.update-request-tag-plural'),
},
};

View File

@ -1,106 +0,0 @@
/*
* 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 { Space, Tooltip } from 'antd';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { EntityField } from 'constants/Feeds.constants';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { getPartialNameFromTableFQN } from 'utils/CommonUtils';
import { ENTITY_LINK_SEPARATOR } from 'utils/EntityUtils';
import { getFieldThreadElement } from 'utils/FeedElementUtils';
import {
getEntityTaskDetails,
getRequestDescriptionPath,
getUpdateDescriptionPath,
} from 'utils/TasksUtils';
import { ReactComponent as IconRequest } from '../../../assets/svg/request-icon.svg';
import { EntityTaskDescriptionProps } from './entityTaskDescription.interface';
const EntityTaskDescription = ({
entityFqn,
entityType,
data,
onThreadLinkSelect,
entityFieldThreads,
}: EntityTaskDescriptionProps) => {
const history = useHistory();
const { t } = useTranslation();
const { fqnPart, entityField } = useMemo(
() => getEntityTaskDetails(entityType),
[entityType]
);
const columnName = useMemo(() => {
const columnName = getPartialNameFromTableFQN(data.fqn ?? '', fqnPart);
return columnName.includes(FQN_SEPARATOR_CHAR)
? `"${columnName}"`
: columnName;
}, [data.fqn]);
const handleDescriptionTask = (hasDescription: boolean) => {
history.push(
(hasDescription ? getUpdateDescriptionPath : getRequestDescriptionPath)(
entityType,
entityFqn,
entityField,
columnName
)
);
};
const requestDescriptionElement = useMemo(() => {
const hasDescription = Boolean(data?.description);
return (
<Tooltip
destroyTooltipOnHide
title={
hasDescription
? t('message.request-update-description')
: t('message.request-description')
}>
<IconRequest
className="cursor-pointer hover-cell-icon"
data-testid="request-description"
height={14}
name={t('message.request-description')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
onClick={() => handleDescriptionTask(hasDescription)}
/>
</Tooltip>
);
}, [data]);
return (
<Space size="middle">
{requestDescriptionElement}
{getFieldThreadElement(
columnName,
EntityField.DESCRIPTION,
entityFieldThreads,
onThreadLinkSelect,
entityType,
entityFqn,
`${entityField}${ENTITY_LINK_SEPARATOR}${columnName}${ENTITY_LINK_SEPARATOR}${EntityField.DESCRIPTION}`
)}
</Space>
);
};
export default EntityTaskDescription;

View File

@ -1,27 +0,0 @@
/*
* 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 { ThreadType } from 'generated/api/feed/createThread';
import { EntityFieldThreads } from 'interface/feed.interface';
export interface EntityTaskDescriptionProps {
data: {
fqn?: string;
description?: string;
};
entityFqn: string;
entityType: EntityType;
entityFieldThreads: EntityFieldThreads[];
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
}

View File

@ -14,6 +14,7 @@
import { Space, Tooltip } from 'antd';
import { FQN_SEPARATOR_CHAR } from 'constants/char.constants';
import { DE_ACTIVE_COLOR } from 'constants/constants';
import { ENTITY_TASKS_TOOLTIP } from 'constants/entity.constants';
import { EntityField } from 'constants/Feeds.constants';
import { TagSource } from 'generated/type/tagLabel';
import { isEmpty } from 'lodash';
@ -25,20 +26,23 @@ import { ENTITY_LINK_SEPARATOR } from 'utils/EntityUtils';
import { getFieldThreadElement } from 'utils/FeedElementUtils';
import {
getEntityTaskDetails,
getRequestDescriptionPath,
getRequestTagsPath,
getUpdateDescriptionPath,
getUpdateTagsPath,
} from 'utils/TasksUtils';
import { ReactComponent as IconRequest } from '../../../assets/svg/request-icon.svg';
import { EntityTaskTagsProps } from './EntityTaskTags.interface';
import { EntityTasksProps } from './EntityTasks.interface';
const EntityTaskTags = ({
const EntityTasks = ({
data,
tagSource,
entityFqn,
entityType,
entityTaskType,
entityFieldThreads,
onThreadLinkSelect,
}: EntityTaskTagsProps) => {
}: EntityTasksProps) => {
const { t } = useTranslation();
const history = useHistory();
@ -55,59 +59,70 @@ const EntityTaskTags = ({
: columnName;
}, [data.fqn]);
const handleTagTask = (hasTags: boolean) => {
history.push(
(hasTags ? getUpdateTagsPath : getRequestTagsPath)(
entityType,
entityFqn,
entityField,
columnName
)
);
const handleTask = (hasData: boolean) => {
if (entityTaskType === EntityField.DESCRIPTION) {
history.push(
(hasData ? getUpdateDescriptionPath : getRequestDescriptionPath)(
entityType,
entityFqn,
entityField,
columnName
)
);
} else {
history.push(
(hasData ? getUpdateTagsPath : getRequestTagsPath)(
entityType,
entityFqn,
entityField,
columnName
)
);
}
};
const getRequestTagsElement = useMemo(() => {
const hasTags = !isEmpty(data.tags);
const taskElement = useMemo(() => {
const hasData = !isEmpty(data.field);
return (
<Tooltip
destroyTooltipOnHide
overlayClassName="ant-popover-request-description"
title={
hasTags
? t('label.update-request-tag-plural')
: t('label.request-tag-plural')
hasData
? ENTITY_TASKS_TOOLTIP[entityTaskType].update
: ENTITY_TASKS_TOOLTIP[entityTaskType].request
}>
<IconRequest
className="hover-cell-icon cursor-pointer"
data-testid="request-tags"
data-testid="task-element"
height={14}
name={t('label.request-tag-plural')}
style={{ color: DE_ACTIVE_COLOR }}
width={14}
onClick={() => handleTagTask(hasTags)}
onClick={() => handleTask(hasData)}
/>
</Tooltip>
);
}, [data]);
}, [data.field]);
return (
<Space size="middle">
{/* Request and Update tags */}
{tagSource === TagSource.Classification && getRequestTagsElement}
<Space data-testid="entity-task" size="middle">
{/* Request and Update Tasks */}
{tagSource !== TagSource.Glossary && taskElement}
{/* List Conversation */}
{getFieldThreadElement(
columnName,
EntityField.TAGS,
entityTaskType,
entityFieldThreads,
onThreadLinkSelect,
entityType,
entityFqn,
`${entityField}${ENTITY_LINK_SEPARATOR}${columnName}${ENTITY_LINK_SEPARATOR}${EntityField.TAGS}`
`${entityField}${ENTITY_LINK_SEPARATOR}${columnName}${ENTITY_LINK_SEPARATOR}${entityTaskType}`
)}
</Space>
);
};
export default EntityTaskTags;
export default EntityTasks;

View File

@ -11,19 +11,21 @@
* limitations under the License.
*/
import { EntityField } from 'constants/Feeds.constants';
import { EntityType } from 'enums/entity.enum';
import { ThreadType } from 'generated/api/feed/createThread';
import { TagLabel, TagSource } from 'generated/type/tagLabel';
import { EntityFieldThreads } from 'interface/feed.interface';
export interface EntityTaskTagsProps {
export interface EntityTasksProps {
data: {
fqn: string;
tags: TagLabel[];
field?: string | TagLabel[];
};
tagSource: TagSource;
tagSource?: TagSource;
entityFqn: string;
entityType: EntityType;
entityTaskType: EntityField.TAGS | EntityField.DESCRIPTION;
entityFieldThreads: EntityFieldThreads[];
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
}

View File

@ -0,0 +1,225 @@
/*
* 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, fireEvent, render, screen } from '@testing-library/react';
import { EntityField } from 'constants/Feeds.constants';
import { EntityType, FqnPart } from 'enums/entity.enum';
import { TagSource } from 'generated/type/tagLabel';
import React from 'react';
import EntityTasks from './EntityTasks.component';
import { EntityTasksProps } from './EntityTasks.interface';
const mockRequestTags = {
pathname: '/request-tags/table/sample_data.ecommerce_db.shopify.fact_sale',
search: 'field=columns&value=sale_id',
};
const mockUpdateTags = {
pathname: '/update-tags/table/sample_data.ecommerce_db.shopify.fact_sale',
search: 'field=columns&value=sale_id',
};
const mockRequestDescription = {
pathname:
'/request-description/table/sample_data.ecommerce_db.shopify.fact_sale',
search: 'field=columns&value=sale_id',
};
const mockUpdateDescription = {
pathname:
'/update-description/table/sample_data.ecommerce_db.shopify.fact_sale',
search: 'field=columns&value=sale_id',
};
const mockProps: EntityTasksProps = {
data: {
fqn: 'sample_data.ecommerce_db.shopify.fact_session',
field: 'this is test',
},
tagSource: TagSource.Classification,
entityFqn: '',
entityType: EntityType.TABLE,
entityTaskType: EntityField.TAGS,
entityFieldThreads: [],
onThreadLinkSelect: jest.fn(),
};
jest.mock('../../../utils/TasksUtils', () => ({
getEntityTaskDetails: jest.fn().mockReturnValue({
fqnPart: FqnPart.NestedColumn,
entityField: EntityField.COLUMNS,
}),
getRequestDescriptionPath: jest
.fn()
.mockImplementation(() => mockRequestDescription),
getRequestTagsPath: jest.fn().mockImplementation(() => mockRequestTags),
getUpdateDescriptionPath: jest
.fn()
.mockImplementation(() => mockUpdateDescription),
getUpdateTagsPath: jest.fn().mockImplementation(() => mockUpdateTags),
}));
jest.mock('../../../utils/FeedElementUtils', () => ({
getFieldThreadElement: jest
.fn()
.mockImplementation(() => (
<p data-testid="list-conversation">List Conversation</p>
)),
}));
jest.mock('../../../utils/CommonUtils', () => ({
getPartialNameFromTableFQN: jest.fn().mockReturnValue('test'),
}));
const mockHistory = {
push: jest.fn(),
};
jest.mock('react-router-dom', () => ({
useHistory: jest.fn().mockImplementation(() => mockHistory),
}));
describe('Entity Task component', () => {
it('Should render the component', async () => {
render(<EntityTasks {...mockProps} />);
const container = await screen.findByTestId('entity-task');
expect(container).toBeInTheDocument();
});
it('Task Element should be visible when tagSource is not glossary', async () => {
render(<EntityTasks {...mockProps} />);
const container = await screen.findByTestId('entity-task');
expect(container).toBeInTheDocument();
const taskElement = screen.queryByTestId('task-element');
expect(taskElement).toBeInTheDocument();
});
it('Task Element should not visible when tagSource is glossary', async () => {
render(<EntityTasks {...mockProps} tagSource={TagSource.Glossary} />);
const container = await screen.findByTestId('entity-task');
expect(container).toBeInTheDocument();
const taskElement = screen.queryByTestId('task-element');
expect(taskElement).not.toBeInTheDocument();
});
it('List conversation should be there in component', async () => {
render(<EntityTasks {...mockProps} tagSource={TagSource.Glossary} />);
const container = await screen.findByTestId('entity-task');
expect(container).toBeInTheDocument();
const conversation = await screen.findByTestId('list-conversation');
expect(conversation).toBeInTheDocument();
});
it('Handle update tags click', async () => {
render(<EntityTasks {...mockProps} />);
const container = await screen.findByTestId('entity-task');
expect(container).toBeInTheDocument();
const taskElement = await screen.findByTestId('task-element');
expect(taskElement).toBeInTheDocument();
await act(async () => {
fireEvent.click(taskElement);
});
expect(mockHistory.push).toHaveBeenCalledWith(mockUpdateTags);
});
it('Handle request tags click', async () => {
render(
<EntityTasks
{...mockProps}
data={{
fqn: 'sample_data.ecommerce_db.shopify.fact_session',
}}
/>
);
const container = await screen.findByTestId('entity-task');
expect(container).toBeInTheDocument();
const taskElement = await screen.findByTestId('task-element');
expect(taskElement).toBeInTheDocument();
await act(async () => {
fireEvent.click(taskElement);
});
expect(mockHistory.push).toHaveBeenCalledWith(mockRequestTags);
});
it('Handle update description click', async () => {
render(
<EntityTasks {...mockProps} entityTaskType={EntityField.DESCRIPTION} />
);
const container = await screen.findByTestId('entity-task');
expect(container).toBeInTheDocument();
const taskElement = await screen.findByTestId('task-element');
expect(taskElement).toBeInTheDocument();
await act(async () => {
fireEvent.click(taskElement);
});
expect(mockHistory.push).toHaveBeenCalledWith(mockUpdateDescription);
});
it('Handle request description click', async () => {
render(
<EntityTasks
{...mockProps}
data={{
fqn: 'sample_data.ecommerce_db.shopify.fact_session',
}}
entityTaskType={EntityField.DESCRIPTION}
/>
);
const container = await screen.findByTestId('entity-task');
expect(container).toBeInTheDocument();
const taskElement = await screen.findByTestId('task-element');
expect(taskElement).toBeInTheDocument();
await act(async () => {
fireEvent.click(taskElement);
});
expect(mockHistory.push).toHaveBeenCalledWith(mockRequestDescription);
});
});