feat(ui): supported separate column for Pipeline entity (#11600)

* Supported separate column for Pipline entity

* fix code smell and bug

* fix code bug

* fix code bug
This commit is contained in:
Ashish Gupta 2023-05-16 19:05:22 +05:30 committed by GitHub
parent 2570ef7795
commit 1df322a704
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 159 additions and 161 deletions

View File

@ -47,6 +47,7 @@ export const TAGS_ADD_REMOVE_ENTITIES = [
serviceName: 'sample_airflow',
fieldName: 'dim_address_task',
tags: ['PersonalData.Personal', 'PII.Sensitive'],
separate: true,
},
// Todo: need to investigate on below test
// more details:- https://cloud.cypress.io/projects/a9yxci/runs/18306/test-results/abe5ab43-84c9-49da-b50f-4936bbcfdd3d

View File

@ -474,7 +474,7 @@ const DashboardDetails = ({
}
};
const handleChartTagSelection = (
const handleChartTagSelection = async (
selectedTags: Array<EntityTags>,
editColumnTag: ChartType,
otherTags: TagLabel[]
@ -505,7 +505,7 @@ const DashboardDetails = ({
tags: [...(prevTags as TagLabel[]), ...newTags],
};
const jsonPatch = compare(editColumnTag, updatedChart);
chartTagUpdateHandler(editColumnTag.id, jsonPatch);
await chartTagUpdateHandler(editColumnTag.id, jsonPatch);
}
};

View File

@ -59,7 +59,10 @@ export interface DashboardDetailsProps {
chartId: string,
patch: Array<Operation>
) => Promise<void>;
chartTagUpdateHandler: (chartId: string, patch: Array<Operation>) => void;
chartTagUpdateHandler: (
chartId: string,
patch: Array<Operation>
) => Promise<void>;
versionHandler: () => void;
postFeedHandler: (value: string, id: string) => void;
deletePostHandler: (

View File

@ -105,11 +105,6 @@ const EntityTable = ({
index: number;
}>();
const [editColumnTag, setEditColumnTag] = useState<{
column: Column;
index: number;
}>();
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
@ -228,14 +223,14 @@ const EntityTable = ({
editColumn.column.fullyQualifiedName,
columnDescription
);
await onUpdate?.(tableCols);
await onUpdate(tableCols);
setEditColumn(undefined);
} else {
setEditColumn(undefined);
}
};
const handleTagSelection = (
const handleTagSelection = async (
selectedTags: EntityTags[],
editColumnTag: Column,
otherTags: TagLabel[]
@ -247,9 +242,8 @@ const EntityTable = ({
if (newSelectedTags && editColumnTag) {
const tableCols = cloneDeep(tableColumns);
updateColumnTags(tableCols, editColumnTag.name, newSelectedTags);
onUpdate?.(tableCols);
await onUpdate(tableCols);
}
setEditColumnTag(undefined);
};
const searchInColumns = (table: Column[], searchText: string): Column[] => {
@ -590,7 +584,6 @@ const EntityTable = ({
entityFieldThreads,
entityFqn,
tableConstraints,
editColumnTag,
isTagLoading,
handleUpdate,
handleTagSelection,

View File

@ -28,7 +28,7 @@ export interface EntityTableProps {
entityFqn?: string;
entityFieldThreads?: EntityFieldThreads[];
entityFieldTasks?: EntityFieldThreads[];
onUpdate?: (columns: Column[]) => Promise<void>;
onUpdate: (columns: Column[]) => Promise<void>;
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
onEntityFieldSelect?: (value: string) => void;
}

View File

@ -22,6 +22,7 @@ import EntityTableV1 from './EntityTable.component';
const onEntityFieldSelect = jest.fn();
const onThreadLinkSelect = jest.fn();
const onUpdate = jest.fn();
const mockTableConstraints = [
{
@ -114,6 +115,7 @@ const mockEntityTableProp = {
tableConstraints: mockTableConstraints,
onEntityFieldSelect,
onThreadLinkSelect,
onUpdate,
};
const mockTagList = [

View File

@ -27,8 +27,10 @@ import { ReactComponent as EditIcon } from 'assets/svg/edit-new.svg';
import { AxiosError } from 'axios';
import { ActivityFilters } from 'components/ActivityFeed/ActivityFeedList/ActivityFeedList.interface';
import { EntityName } from 'components/Modals/EntityNameModal/EntityNameModal.interface';
import TableTags from 'components/TableTags/TableTags.component';
import { compare, Operation } from 'fast-json-patch';
import { isEmpty, isUndefined } from 'lodash';
import { TagSource } from 'generated/type/schema';
import { isEmpty, isUndefined, map } from 'lodash';
import { EntityTags, ExtraInfo, TagOption } from 'Models';
import React, {
RefObject,
@ -41,6 +43,8 @@ import { useTranslation } from 'react-i18next';
import { Link, Redirect, useHistory, useParams } from 'react-router-dom';
import { getAllFeeds, postFeedById, postThread } from 'rest/feedsAPI';
import { restorePipeline } from 'rest/pipelineAPI';
import { fetchGlossaryTerms, getGlossaryTermlist } from 'utils/GlossaryUtils';
import { getFilterTags } from 'utils/TableTags/TableTags.utils';
import AppState from '../../AppState';
import { ReactComponent as ExternalLinkIcon } from '../../assets/svg/external-link.svg';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
@ -87,7 +91,7 @@ import {
} from '../../utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
import { fetchTagsAndGlossaryTerms } from '../../utils/TagsUtils';
import { getClassifications, getTaglist } from '../../utils/TagsUtils';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList';
import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
@ -103,8 +107,6 @@ import Loader from '../Loader/Loader';
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
import TagsContainer from '../Tag/TagsContainer/tags-container';
import TagsViewer from '../Tag/TagsViewer/tags-viewer';
import TasksDAGView from '../TasksDAGView/TasksDAGView';
import { PipeLineDetailsProp } from './PipelineDetails.interface';
@ -150,10 +152,7 @@ const PipelineDetails = ({
}, [pipelineDetails]);
// local state variables
const [editTaskTags, setEditTaskTags] = useState<{
task: Task;
index: number;
}>();
const [isEdit, setIsEdit] = useState(false);
const [followersCount, setFollowersCount] = useState(0);
const [isFollowing, setIsFollowing] = useState(false);
@ -175,8 +174,6 @@ const PipelineDetails = ({
EntityFieldThreadCount[]
>([]);
const [tagList, setTagList] = useState<TagOption[]>();
const [threadLink, setThreadLink] = useState<string>('');
const [elementRef, isInView] = useElementInView(observerOptions);
@ -195,6 +192,11 @@ const PipelineDetails = ({
const [activityFilter, setActivityFilter] = useState<ActivityFilters>();
const [isTagLoading, setIsTagLoading] = useState<boolean>(false);
const [tagFetchFailed, setTagFetchFailed] = useState<boolean>(false);
const [glossaryTags, setGlossaryTags] = useState<TagOption[]>([]);
const [classificationTags, setClassificationTags] = useState<TagOption[]>([]);
// local state ends
const USERId = getCurrentUserId();
@ -213,6 +215,11 @@ const PipelineDetails = ({
[entityThreadLoading]
);
const hasTagEditAccess = useMemo(
() => pipelinePermissions.EditAll || pipelinePermissions.EditTags,
[pipelinePermissions]
);
const getEntityFeedCount = () => {
getFeedCounts(
EntityType.PIPELINE,
@ -239,6 +246,41 @@ const PipelineDetails = ({
}
}, [pipelineDetails.id, getEntityPermission, setPipelinePermissions]);
const fetchGlossaryTags = async () => {
setIsTagLoading(true);
try {
const res = await fetchGlossaryTerms();
const glossaryTerms: TagOption[] = getGlossaryTermlist(res).map(
(tag) => ({ fqn: tag, source: TagSource.Glossary })
);
setGlossaryTags(glossaryTerms);
} catch {
setTagFetchFailed(true);
} finally {
setIsTagLoading(false);
}
};
const fetchClassificationTags = async () => {
setIsTagLoading(true);
try {
const res = await getClassifications();
const tagList = await getTaglist(res.data);
const classificationTag: TagOption[] = map(tagList, (tag) => ({
fqn: tag,
source: TagSource.Classification,
}));
setClassificationTags(classificationTag);
} catch {
setTagFetchFailed(true);
} finally {
setIsTagLoading(false);
}
};
useEffect(() => {
if (pipelineDetails.id) {
fetchResourcePermission();
@ -497,105 +539,47 @@ const PipelineDetails = ({
getFeedData(undefined, feedFilter, threadType);
}, []);
const handleEditTaskTag = (task: Task, index: number): void => {
setEditTaskTags({ task: { ...task, tags: [] }, index });
};
const handleTableTagSelection = (
selectedTags: Array<EntityTags> = [],
task: {
task: Task;
index: number;
}
const handleTableTagSelection = async (
selectedTags: EntityTags[],
editColumnTag: Task,
otherTags: TagLabel[]
) => {
const selectedTask = isUndefined(editTask) ? task : editTask;
const prevTags = selectedTask.task.tags?.filter((tag) =>
selectedTags.some((selectedTag) => selectedTag.tagFQN === tag.tagFQN)
const newSelectedTags: TagOption[] = map(
[...selectedTags, ...otherTags],
(tag) => ({ fqn: tag.tagFQN, source: tag.source })
);
const newTags = selectedTags
const prevTags = editColumnTag.tags?.filter((tag) =>
newSelectedTags.some((selectedTag) => selectedTag.fqn === tag.tagFQN)
);
const newTags = newSelectedTags
.filter(
(selectedTag) =>
!selectedTask.task.tags?.some(
(tag) => tag.tagFQN === selectedTag.tagFQN
)
!editColumnTag.tags?.some((tag) => tag.tagFQN === selectedTag.fqn)
)
.map((tag) => ({
labelType: 'Manual',
state: 'Confirmed',
source: tag.source,
tagFQN: tag.tagFQN,
tagFQN: tag.fqn,
}));
const updatedTasks: Task[] = [...(pipelineDetails.tasks || [])];
const updatedTask = {
...selectedTask.task,
...editColumnTag,
tags: [...(prevTags as TagLabel[]), ...newTags],
} as Task;
updatedTasks[selectedTask.index] = updatedTask;
const updatedTasks: Task[] = [...(pipelineDetails.tasks ?? [])].map(
(task) => (task.name === editColumnTag.name ? updatedTask : task)
);
const updatedPipeline = { ...pipelineDetails, tasks: updatedTasks };
const jsonPatch = compare(pipelineDetails, updatedPipeline);
taskUpdateHandler(jsonPatch);
setEditTaskTags(undefined);
await taskUpdateHandler(jsonPatch);
};
useMemo(() => {
fetchTagsAndGlossaryTerms().then((response) => {
setTagList(response);
});
}, [setTagList]);
const addButtonHandler = useCallback((record, index) => {
handleEditTaskTag(record, index);
}, []);
const renderTags = useCallback(
(tags, record, index) => (
<div className="relative tableBody-cell" data-testid="tags-wrapper">
{deleted ? (
<TagsViewer sizeCap={-1} tags={tags || []} />
) : (
<TagsContainer
editable={editTaskTags?.index === index}
selectedTags={tags as EntityTags[]}
showAddTagButton={
(pipelinePermissions.EditAll || pipelinePermissions.EditTags) &&
isEmpty(tags)
}
showEditTagButton={
pipelinePermissions.EditAll || pipelinePermissions.EditTags
}
size="small"
tagList={tagList ?? []}
type="label"
onAddButtonClick={() => addButtonHandler(record, index)}
onCancel={() => {
setEditTask(undefined);
}}
onEditButtonClick={() => addButtonHandler(record, index)}
onSelectionChange={(tags) => {
handleTableTagSelection(tags, {
task: record,
index: index,
});
}}
/>
)}
</div>
),
[
tagList,
editTaskTags,
pipelinePermissions.EditAll,
pipelinePermissions.EditTags,
deleted,
]
);
const taskColumns: ColumnsType<Task> = useMemo(
() => [
{
@ -669,14 +653,65 @@ const PipelineDetails = ({
),
},
{
key: t('label.tag-plural'),
dataIndex: 'tags',
title: t('label.tag-plural'),
width: 350,
render: renderTags,
dataIndex: 'tags',
key: 'tags',
accessor: 'tags',
width: 300,
render: (tags, record, index) => (
<TableTags<Task>
dataTestId="classification-tags"
fetchTags={fetchClassificationTags}
handleTagSelection={handleTableTagSelection}
hasTagEditAccess={hasTagEditAccess}
index={index}
isReadOnly={deleted}
isTagLoading={isTagLoading}
record={record}
tagFetchFailed={tagFetchFailed}
tagList={classificationTags}
tags={getFilterTags(tags)}
type={TagSource.Classification}
/>
),
},
{
title: t('label.glossary-term-plural'),
dataIndex: 'tags',
key: 'tags',
accessor: 'tags',
width: 300,
render: (tags, record, index) => (
<TableTags<Task>
dataTestId="glossary-tags"
fetchTags={fetchGlossaryTags}
handleTagSelection={handleTableTagSelection}
hasTagEditAccess={hasTagEditAccess}
index={index}
isReadOnly={deleted}
isTagLoading={isTagLoading}
record={record}
tagFetchFailed={tagFetchFailed}
tagList={glossaryTags}
tags={getFilterTags(tags)}
type={TagSource.Glossary}
/>
),
},
],
[pipelinePermissions, editTask, editTaskTags, tagList, deleted]
[
fetchGlossaryTags,
fetchClassificationTags,
handleTableTagSelection,
classificationTags,
hasTagEditAccess,
pipelinePermissions,
editTask,
deleted,
isTagLoading,
tagFetchFailed,
glossaryTags,
]
);
useEffect(() => {

View File

@ -12,6 +12,7 @@
*/
import {
findAllByTestId,
findByTestId,
findByText,
fireEvent,
@ -19,7 +20,6 @@ import {
render,
screen,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { act } from 'react-test-renderer';
@ -81,17 +81,6 @@ const mockTasks = [
},
];
const mockTags = [
{
tagFQN: 'PII.Sensitive',
source: 'Tag',
},
{
tagFQN: 'PersonalData.Personal',
source: 'Tag',
},
];
const mockTaskUpdateHandler = jest.fn();
const PipelineDetailsProps = {
@ -147,14 +136,6 @@ jest.mock('../common/rich-text-editor/RichTextEditorPreviewer', () => {
return jest.fn().mockReturnValue(<p>RichTextEditorPreviwer</p>);
});
jest.mock('components/Tag/TagsContainer/tags-container', () => {
return jest.fn().mockReturnValue(<p>Tag Container</p>);
});
jest.mock('components/Tag/Tags/tags', () => {
return jest.fn().mockReturnValue(<p>Tags</p>);
});
jest.mock('../EntityLineage/EntityLineage.component', () => {
return jest.fn().mockReturnValue(<p>EntityLineage</p>);
});
@ -201,17 +182,12 @@ jest.mock('../../utils/CommonUtils', () => ({
jest.mock('../Execution/Execution.component', () => {
return jest.fn().mockImplementation(() => <p>Executions</p>);
});
jest.mock('../Tag/TagsContainer/tags-container', () =>
jest.fn().mockImplementation(({ onSelectionChange }) => (
<div data-testid="tags-container">
<div
data-testid="onSelectionChange"
onClick={() => onSelectionChange(mockTags)}>
onSelectionChange
</div>
</div>
))
jest.mock('components/TableTags/TableTags.component', () =>
jest
.fn()
.mockImplementation(() => (
<div data-testid="table-tag-container">Table Tag Container</div>
))
);
describe('Test PipelineDetails component', () => {
@ -235,6 +211,10 @@ describe('Test PipelineDetails component', () => {
container,
'label.custom-property-plural'
);
const tagsContainer = await findAllByTestId(
container,
'table-tag-container'
);
expect(EntityPageInfo).toBeInTheDocument();
expect(description).toBeInTheDocument();
@ -243,6 +223,7 @@ describe('Test PipelineDetails component', () => {
expect(lineageTab).toBeInTheDocument();
expect(executionsTab).toBeInTheDocument();
expect(customPropertiesTab).toBeInTheDocument();
expect(tagsContainer).toHaveLength(4);
});
it('Check if active tab is tasks', async () => {
@ -381,22 +362,4 @@ describe('Test PipelineDetails component', () => {
expect(obServerElement).toBeInTheDocument();
});
it('taskUpdateHandler should be called after the tags are added or removed to a task', async () => {
render(<PipelineDetails {...PipelineDetailsProps} />, {
wrapper: MemoryRouter,
});
const tagsContainer = screen.getAllByTestId('tags-container');
expect(tagsContainer).toHaveLength(2);
const onSelectionChange = screen.getAllByTestId('onSelectionChange');
expect(onSelectionChange).toHaveLength(2);
await act(async () => userEvent.click(onSelectionChange[0]));
expect(mockTaskUpdateHandler).toHaveBeenCalledTimes(1);
});
});

View File

@ -33,5 +33,5 @@ export type Props = {
entityFieldTasks?: EntityFieldThreads[];
onThreadLinkSelect?: (value: string, threadType?: ThreadType) => void;
onEntityFieldSelect?: (value: string) => void;
onUpdate?: (columns: Table['columns']) => Promise<void>;
onUpdate: (columns: Table['columns']) => Promise<void>;
};

View File

@ -140,8 +140,8 @@ const TableTags = <T extends TableUnion>({
type="label"
onAddButtonClick={addButtonHandler}
onCancel={() => setIsEdit(false)}
onSelectionChange={(selectedTags) => {
handleTagSelection(selectedTags, record, otherTags);
onSelectionChange={async (selectedTags) => {
await handleTagSelection(selectedTags, record, otherTags);
setIsEdit(false);
}}
/>

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { Task } from 'generated/entity/data/pipeline';
import { Field } from 'generated/entity/data/topic';
import { TagLabel, TagSource } from 'generated/type/tagLabel';
import { EntityTags, TagOption } from 'Models';
@ -33,7 +34,7 @@ export interface TableTagsComponentProps<T> {
selectedTags: Array<EntityTags>,
editColumnTag: T,
otherTags: TagLabel[]
) => void;
) => Promise<void>;
onRequestTagsHandler?: (cell: T) => void;
getColumnName?: (cell: T) => string;
getColumnFieldFQN?: string;
@ -42,7 +43,7 @@ export interface TableTagsComponentProps<T> {
entityFieldThreads?: EntityFieldThreads[];
tagFetchFailed: boolean;
type: TagSource;
fetchTags: () => void;
fetchTags: () => Promise<void>;
dataTestId: string;
}
@ -56,4 +57,4 @@ export interface TableTagsProps {
Glossary: TagLabel[];
}
export type TableUnion = Column | Field | ChartType;
export type TableUnion = Column | Field | Task | ChartType;