fix(ui): pipelineDetails page update (#8582)

* fix(ui): pipelineDetails page update

* fix Lineage and Activity & Task tab UI

* complete ui changes for pipelineDetails

* fix failing tests
add localizations

* fix tree view on execution tab

* fix wrapping for tree nodes

* comment addressed

* added missed localization

* add test ids for tabs

* fix cypress failing for ingestion

* fix missing condition
This commit is contained in:
Chirag Madlani 2022-11-10 21:37:35 +05:30 committed by GitHub
parent 2e158237ea
commit d4b2621e9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 860 additions and 616 deletions

View File

@ -625,6 +625,7 @@ const DashboardDetails = ({
editChartTags,
tagList,
deleted,
isTagLoading,
]
);
@ -782,7 +783,7 @@ const DashboardDetails = ({
dashboardDetails as CustomPropertyProps['entityDetails']
}
entityType={EntityType.DASHBOARD}
handleExtentionUpdate={onExtensionUpdate}
handleExtensionUpdate={onExtensionUpdate}
/>
)}
<div

View File

@ -128,7 +128,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
deletePostHandler,
paging,
fetchFeedHandler,
handleExtentionUpdate,
handleExtensionUpdate,
updateThreadHandler,
entityFieldTaskCount,
}: DatasetDetailsProps) => {
@ -834,7 +834,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
tableDetails as CustomPropertyProps['entityDetails']
}
entityType={EntityType.TABLE}
handleExtentionUpdate={handleExtentionUpdate}
handleExtensionUpdate={handleExtensionUpdate}
/>
)}
<div

View File

@ -95,6 +95,6 @@ export interface DatasetDetailsProps {
feedType?: FeedFilter,
threadType?: ThreadType
) => void;
handleExtentionUpdate: (updatedTable: Table) => Promise<void>;
handleExtensionUpdate: (updatedTable: Table) => Promise<void>;
updateThreadHandler: ThreadUpdatedFunc;
}

View File

@ -174,7 +174,7 @@ const DatasetDetailsProps = {
deletePostHandler: jest.fn(),
tagUpdateHandler: jest.fn(),
fetchFeedHandler: jest.fn(),
handleExtentionUpdate: jest.fn(),
handleExtensionUpdate: jest.fn(),
updateThreadHandler: jest.fn(),
};

View File

@ -10,11 +10,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Card, Col, Empty, Row, Space, Typography } from 'antd';
import { Card, Col, Empty, Row, Space, Tooltip, Typography } from 'antd';
import { isEmpty, uniqueId } from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Tooltip } from 'react-tippy';
import { PipelineStatus } from '../../../generated/entity/data/pipeline';
import { getTreeViewData } from '../../../utils/executionUtils';
import { getStatusBadgeIcon } from '../../../utils/PipelineDetailsUtils';
@ -80,25 +79,25 @@ const TreeViewTab = ({
</Col>
<Col span={19}>
<Space>
<div className="execution-node-container">
{value.map((status) => (
<Tooltip
html={
key={uniqueId()}
placement="top"
title={
<Space direction="vertical">
<div>{status.timestamp}</div>
<div>{status.executionStatus}</div>
</Space>
}
key={uniqueId()}
position="bottom">
}>
<SVGIcons
alt="result"
className="tw-w-6"
className="tw-w-6 mr-2 mb-2"
icon={getStatusBadgeIcon(status.executionStatus)}
/>
</Tooltip>
))}
</Space>
</div>
</Col>
</Row>
);

View File

@ -12,7 +12,7 @@
*/
import { Popover, Skeleton, Space } from 'antd';
import { capitalize } from 'lodash';
import { capitalize, isEmpty } from 'lodash';
import React, {
FunctionComponent,
useCallback,
@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next';
import { getRunHistoryForPipeline } from '../../../axiosAPIs/ingestionPipelineAPI';
import {
IngestionPipeline,
PipelineState,
PipelineStatus,
} from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import {
@ -45,7 +46,7 @@ export const IngestionRecentRuns: FunctionComponent<Props> = ({
classNames,
}: Props) => {
const { t } = useTranslation();
const [recentRunStatus, setRecentRunStatus] = useState<PipelineStatus[]>();
const [recentRunStatus, setRecentRunStatus] = useState<PipelineStatus[]>([]);
const [loading, setLoading] = useState(true);
const fetchPipelineStatus = useCallback(async () => {
@ -59,8 +60,8 @@ export const IngestionRecentRuns: FunctionComponent<Props> = ({
const runs = response.data.splice(0, 5).reverse() ?? [];
setRecentRunStatus(
runs.length === 0
? [ingestion.pipelineStatuses as PipelineStatus]
runs.length === 0 && ingestion.pipelineStatuses
? [ingestion.pipelineStatuses]
: runs
);
} finally {
@ -78,8 +79,14 @@ export const IngestionRecentRuns: FunctionComponent<Props> = ({
<Space className={classNames} size={2}>
{loading ? (
<Skeleton.Input size="small" />
) : isEmpty(recentRunStatus) ? (
<p
className={`tw-h-5 tw-w-16 tw-rounded-sm tw-bg-status-${PipelineState.Queued} tw-px-1 tw-text-white tw-text-center`}
data-testid="pipeline-status">
{capitalize(PipelineState.Queued)}
</p>
) : (
recentRunStatus?.map((r, i) => {
recentRunStatus.map((r, i) => {
const status =
i === recentRunStatus.length - 1 ? (
<p

View File

@ -664,7 +664,7 @@ const MlModelDetail: FC<MlModelDetailProp> = ({
mlModelDetail as CustomPropertyProps['entityDetails']
}
entityType={EntityType.MLMODEL}
handleExtentionUpdate={onExtensionUpdate}
handleExtensionUpdate={onExtensionUpdate}
/>
)}
<div

View File

@ -11,39 +11,77 @@
* limitations under the License.
*/
import { compare } from 'fast-json-patch';
import { Card, Col, Row, Space, Table, Tabs, Tooltip } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { AxiosError } from 'axios';
import { compare, Operation } from 'fast-json-patch';
import { isEmpty } from 'lodash';
import { EntityTags, ExtraInfo } from 'Models';
import React, { RefObject, useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
EntityFieldThreadCount,
EntityTags,
ExtraInfo,
TagOption,
} from 'Models';
import React, {
RefObject,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { Link, Redirect, useHistory, useParams } from 'react-router-dom';
import AppState from '../../AppState';
import {
getAllFeeds,
postFeedById,
postThread,
} from '../../axiosAPIs/feedsAPI';
import { getLineageByFQN } from '../../axiosAPIs/lineageAPI';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { getPipelineDetailsPath, ROUTES } from '../../constants/constants';
import { EntityField } from '../../constants/feed.constants';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { observerOptions } from '../../constants/Mydata.constants';
import { PIPELINE_DETAILS_TABS } from '../../constants/pipeline.constants';
import { EntityType } from '../../enums/entity.enum';
import { FeedFilter } from '../../enums/mydata.enum';
import { OwnerType } from '../../enums/user.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
import {
Pipeline,
PipelineStatus,
TagLabel,
Task,
} from '../../generated/entity/data/pipeline';
import { ThreadType } from '../../generated/entity/feed/thread';
import { Post, Thread, ThreadType } from '../../generated/entity/feed/thread';
import { EntityLineage } from '../../generated/type/entityLineage';
import { EntityReference } from '../../generated/type/entityReference';
import { Paging } from '../../generated/type/paging';
import { LabelType, State } from '../../generated/type/tagLabel';
import { useInfiniteScroll } from '../../hooks/useInfiniteScroll';
import jsonData from '../../jsons/en';
import {
getCountBadge,
getCurrentUserId,
getEntityName,
getEntityPlaceHolder,
getFeedCounts,
getOwnerValue,
} from '../../utils/CommonUtils';
import { getEntityFeedLink } from '../../utils/EntityUtils';
import { getDefaultValue } from '../../utils/FeedElementUtils';
import { getEntityFieldThreadCounts } from '../../utils/FeedUtils';
import {
deletePost,
getEntityFieldThreadCounts,
updateThreadData,
} from '../../utils/FeedUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { getLineageViewPath } from '../../utils/RouterUtils';
import { getTagsWithoutTier } from '../../utils/TableUtils';
import SVGIcons from '../../utils/SvgUtils';
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
import { fetchTagsAndGlossaryTerms } from '../../utils/TagsUtils';
import { getDateTimeByTimeStamp } from '../../utils/TimeUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList';
import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
@ -51,7 +89,7 @@ import { CustomPropertyTable } from '../common/CustomPropertyTable/CustomPropert
import { CustomPropertyProps } from '../common/CustomPropertyTable/CustomPropertyTable.interface';
import Description from '../common/description/Description';
import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo';
import TabsPane from '../common/TabsPane/TabsPane';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import PageContainer from '../containers/PageContainer';
import EntityLineageComponent from '../EntityLineage/EntityLineage.component';
import ExecutionsTab from '../Execution/Execution.component';
@ -60,23 +98,17 @@ import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/Modal
import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
import TagsContainer from '../tags-container/tags-container';
import TagsViewer from '../tags-viewer/tags-viewer';
import TasksDAGView from '../TasksDAGView/TasksDAGView';
import { PipeLineDetailsProp } from './PipelineDetails.interface';
const PipelineDetails = ({
entityName,
owner,
tier,
slashedPipelineName,
pipelineTags,
activeTab,
pipelineUrl,
pipelineDetails,
serviceType,
setActiveTabHandler,
description,
descriptionUpdateHandler,
entityLineage,
followers,
followPipelineHandler,
unfollowPipelineHandler,
@ -87,29 +119,43 @@ const PipelineDetails = ({
loadNodeHandler,
lineageLeafNodes,
isNodeLoading,
version,
deleted,
versionHandler,
addLineageHandler,
removeLineageHandler,
entityLineageHandler,
isLineageLoading,
isentityThreadLoading,
entityThread,
postFeedHandler,
feedCount,
entityFieldThreadCount,
createThread,
pipelineFQN,
deletePostHandler,
paging,
fetchFeedHandler,
pipelineStatus,
updateThreadHandler,
entityFieldTaskCount,
onExtensionUpdate,
}: PipeLineDetailsProp) => {
const history = useHistory();
const { tab } = useParams<{ tab: PIPELINE_DETAILS_TABS }>();
const { t } = useTranslation();
const {
tier,
deleted,
owner,
serviceType,
description,
version,
pipelineStatus,
tags,
} = useMemo(() => {
return {
deleted: pipelineDetails.deleted,
owner: pipelineDetails.owner,
serviceType: pipelineDetails.serviceType,
description: pipelineDetails.description,
version: pipelineDetails.version,
pipelineStatus: pipelineDetails.pipelineStatus,
tier: getTierTags(pipelineDetails.tags ?? []),
tags: getTagsWithoutTier(pipelineDetails.tags ?? []),
};
}, [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);
@ -117,6 +163,25 @@ const PipelineDetails = ({
task: Task;
index: number;
}>();
const [lineageLoading, setLineageLoading] = useState(false);
const [entityLineage, setEntityLineage] = useState<EntityLineage>(
{} as EntityLineage
);
const [entityThreadLoading, setEntityThreadLoading] = useState(false);
const [entityThreads, setEntityThreads] = useState<Thread[]>([]);
const [entityThreadPaging, setEntityThreadPaging] = useState<Paging>({
total: 0,
} as Paging);
const [feedCount, setFeedCount] = useState<number>(0);
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
EntityFieldThreadCount[]
>([]);
const [entityFieldTaskCount, setEntityFieldTaskCount] = useState<
EntityFieldThreadCount[]
>([]);
const [tagList, setTagList] = useState<TagOption[]>();
const [threadLink, setThreadLink] = useState<string>('');
@ -134,8 +199,20 @@ const PipelineDetails = ({
DEFAULT_ENTITY_PERMISSION
);
// local state ends
const USERId = getCurrentUserId();
const { getEntityPermission } = usePermissionProvider();
const tasksInternal = useMemo(
() => tasks.map((t) => ({ ...t, tags: t.tags ?? [] })),
[tasks]
);
const onEntityFieldSelect = (value: string) => {
setSelectedField(value);
};
const fetchResourcePermission = useCallback(async () => {
try {
const entityPermission = await getEntityPermission(
@ -156,9 +233,6 @@ const PipelineDetails = ({
}
}, [pipelineDetails.id]);
const onEntityFieldSelect = (value: string) => {
setSelectedField(value);
};
const closeRequestModal = () => {
setSelectedField('');
};
@ -169,63 +243,11 @@ const PipelineDetails = ({
);
setFollowersCount(followers?.length);
};
const tabs = [
{
name: 'Details',
icon: {
alt: 'schema',
name: 'icon-schema',
title: 'Details',
selectedName: 'icon-schemacolor',
},
isProtected: false,
position: 1,
},
{
name: 'Activity Feeds & Tasks',
icon: {
alt: 'activity_feed',
name: 'activity_feed',
title: 'Activity Feed',
selectedName: 'activity-feed-color',
},
isProtected: false,
position: 2,
count: feedCount,
},
{
name: 'Executions',
icon: {
alt: 'executions',
name: 'executions',
title: 'Executions',
selectedName: 'activity-feed-color',
},
isProtected: false,
position: 3,
},
{
name: 'Lineage',
icon: {
alt: 'lineage',
name: 'icon-lineage',
title: 'Lineage',
selectedName: 'icon-lineagecolor',
},
isProtected: false,
position: 4,
},
{
name: 'Custom Properties',
isProtected: false,
position: 5,
},
];
const extraInfo: Array<ExtraInfo> = [
{
key: 'Owner',
value: getOwnerValue(owner),
value: owner && getOwnerValue(owner),
placeholderText: getEntityPlaceHolder(
getEntityName(owner),
owner?.deleted
@ -378,7 +400,41 @@ const PipelineDetails = ({
};
const getLoader = () => {
return isentityThreadLoading ? <Loader /> : null;
return entityThreadLoading ? <Loader /> : null;
};
const getFeedData = (
after?: string,
feedFilter?: FeedFilter,
threadType?: ThreadType
) => {
setEntityThreadLoading(true);
getAllFeeds(
getEntityFeedLink(EntityType.PIPELINE, pipelineFQN),
after,
threadType,
feedFilter,
undefined,
USERId
)
.then((res) => {
const { data, paging: pagingObj } = res;
if (data) {
setEntityThreadPaging(pagingObj);
setEntityThreads((prevData) => [...prevData, ...data]);
} else {
showErrorToast(
jsonData['api-error-messages']['fetch-entity-feed-error']
);
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['fetch-entity-feed-error']
);
})
.finally(() => setEntityThreadLoading(false));
};
const fetchMoreThread = (
@ -387,7 +443,7 @@ const PipelineDetails = ({
isLoading: boolean
) => {
if (isElementInView && pagingObj?.after && !isLoading) {
fetchFeedHandler(pagingObj.after);
getFeedData(pagingObj.after);
}
};
@ -396,16 +452,313 @@ const PipelineDetails = ({
}, [followers]);
useEffect(() => {
fetchMoreThread(isInView as boolean, paging, isentityThreadLoading);
}, [paging, isentityThreadLoading, isInView]);
fetchMoreThread(
isInView as boolean,
entityThreadPaging,
entityThreadLoading
);
}, [entityThreadPaging, entityThreadLoading, isInView]);
const handleFeedFilterChange = useCallback(
(feedFilter, threadType) => {
fetchFeedHandler(paging.after, feedFilter, threadType);
getFeedData(entityThreadPaging.after, feedFilter, threadType);
},
[paging]
[entityThreadPaging]
);
const handleEditTaskTag = (task: Task, index: number): void => {
setEditTaskTags({ task: { ...task, tags: [] }, index });
};
const handleTableTagSelection = (selectedTags?: Array<EntityTags>) => {
if (selectedTags && editTask) {
const prevTags = editTask.task.tags?.filter((tag) =>
selectedTags.some((selectedTag) => selectedTag.tagFQN === tag.tagFQN)
);
const newTags = selectedTags
.filter(
(selectedTag) =>
!editTask.task.tags?.some(
(tag) => tag.tagFQN === selectedTag.tagFQN
)
)
.map((tag) => ({
labelType: 'Manual',
state: 'Confirmed',
source: tag.source,
tagFQN: tag.tagFQN,
}));
const updatedTasks: Task[] = [...(pipelineDetails.tasks || [])];
const updatedTask = {
...editTask.task,
tags: [...(prevTags as TagLabel[]), ...newTags],
} as Task;
updatedTasks[editTask.index] = updatedTask;
const updatedPipeline = { ...pipelineDetails, tasks: updatedTasks };
const jsonPatch = compare(pipelineDetails, updatedPipeline);
taskUpdateHandler(jsonPatch);
}
setEditTaskTags(undefined);
};
useMemo(() => {
fetchTagsAndGlossaryTerms().then((response) => {
setTagList(response);
});
}, [setTagList]);
const renderTags = useCallback(
(text, record, index) => (
<div
className="relative tableBody-cell"
data-testid="tags-wrapper"
onClick={() => handleEditTaskTag(record, index)}>
{deleted ? (
<div className="tw-flex tw-flex-wrap">
<TagsViewer sizeCap={-1} tags={text || []} />
</div>
) : (
<TagsContainer
editable={editTaskTags?.index === index}
selectedTags={text as EntityTags[]}
showAddTagButton={
pipelinePermissions.EditAll || pipelinePermissions.EditTags
}
size="small"
tagList={tagList ?? []}
type="label"
onCancel={() => {
handleTableTagSelection();
}}
onSelectionChange={(tags) => {
handleTableTagSelection(tags);
}}
/>
)}
</div>
),
[
tagList,
editTaskTags,
pipelinePermissions.EditAll,
pipelinePermissions.EditTags,
deleted,
]
);
const taskColumns: ColumnsType<Task> = useMemo(
() => [
{
key: 'name',
dataIndex: 'name',
title: t('label.name'),
render: (name, record) => (
<Link target="_blank" to={{ pathname: record.taskUrl }}>
<span>{name}</span>
<SVGIcons
alt="external-link"
className="align-middle m-l-xs"
icon="external-link"
width="16px"
/>
</Link>
),
},
{
key: 'type',
dataIndex: 'taskType',
width: 180,
title: t('label.type'),
},
{
key: 'startDate',
dataIndex: 'startDate',
width: 180,
title: t('label.start-date'),
render: (startDate: string) =>
getDateTimeByTimeStamp(new Date(startDate).valueOf()),
},
{
key: 'description',
dataIndex: 'description',
width: 350,
title: t('label.description'),
render: (text, record, index) => (
<Space
className="w-full tw-group cursor-pointer"
data-testid="description">
<div>
{text ? (
<RichTextEditorPreviewer markdown={text} />
) : (
<span className="tw-no-description">No description</span>
)}
</div>
{!deleted && (
<Tooltip
title={
pipelinePermissions.EditAll
? 'Edit Description'
: NO_PERMISSION_FOR_ACTION
}>
<button
className="tw-self-start tw-w-8 tw-h-auto tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none"
disabled={!pipelinePermissions.EditAll}
onClick={() => setEditTask({ task: record, index })}>
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="16px"
/>
</button>
</Tooltip>
)}
</Space>
),
},
{
key: 'tags',
dataIndex: 'tags',
title: t('label.tags'),
width: 350,
render: renderTags,
},
],
[pipelinePermissions, editTask, editTaskTags, tagList, deleted]
);
const getLineageData = () => {
setLineageLoading(true);
getLineageByFQN(pipelineFQN, EntityType.PIPELINE)
.then((res) => {
if (res) {
setEntityLineage(res);
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['fetch-lineage-error']
);
})
.finally(() => {
setLineageLoading(false);
});
};
useEffect(() => {
switch (tab) {
case PIPELINE_DETAILS_TABS.EntityLineage:
!deleted && isEmpty(entityLineage) && getLineageData();
break;
case PIPELINE_DETAILS_TABS.ActivityFeedsAndTasks:
getFeedData();
break;
default:
break;
}
}, [tab]);
const handleTabChange = (tabValue: string) => {
if (tabValue !== tab) {
history.push({
pathname: getPipelineDetailsPath(pipelineFQN, tabValue),
});
}
};
const getEntityFeedCount = () => {
getFeedCounts(
EntityType.PIPELINE,
pipelineFQN,
setEntityFieldThreadCount,
setEntityFieldTaskCount,
setFeedCount
);
};
const postFeedHandler = (value: string, id: string) => {
const currentUser = AppState.userDetails?.name ?? AppState.users[0]?.name;
const data = {
message: value,
from: currentUser,
} as Post;
postFeedById(id, data)
.then((res) => {
if (res) {
const { id, posts } = res;
setEntityThreads((pre) => {
return pre.map((thread) => {
if (thread.id === id) {
return { ...res, posts: posts?.slice(-3) };
} else {
return thread;
}
});
});
getEntityFeedCount();
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(err, jsonData['api-error-messages']['add-feed-error']);
});
};
const createThread = (data: CreateThread) => {
postThread(data)
.then((res) => {
if (res) {
setEntityThreads((pre) => [...pre, res]);
getEntityFeedCount();
} else {
showErrorToast(
jsonData['api-error-messages']['unexpected-server-response']
);
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['create-conversation-error']
);
});
};
const deletePostHandler = (
threadId: string,
postId: string,
isThread: boolean
) => {
deletePost(threadId, postId, isThread, setEntityThreads);
};
const updateThreadHandler = (
threadId: string,
postId: string,
isThread: boolean,
data: Operation[]
) => {
updateThreadData(threadId, postId, isThread, data, setEntityThreads);
};
useEffect(() => {
getEntityFeedCount();
}, [pipelineFQN, description, pipelineDetails, tasks]);
return (
<PageContainer>
<div className="tw-px-6 tw-w-full tw-h-full tw-flex tw-flex-col">
@ -443,7 +796,7 @@ const PipelineDetails = ({
? onTierRemove
: undefined
}
tags={pipelineTags}
tags={tags}
tagsHandler={onTagUpdate}
tier={tier}
titleLinks={slashedPipelineName}
@ -457,139 +810,187 @@ const PipelineDetails = ({
? onTierUpdate
: undefined
}
version={version}
version={version + ''}
versionHandler={versionHandler}
onThreadLinkSelect={onThreadLinkSelect}
/>
<div className="tw-mt-4 tw-flex tw-flex-col tw-flex-grow tw-w-full">
<TabsPane
activeTab={activeTab}
setActiveTab={setActiveTabHandler}
tabs={tabs}
/>
<div className="tw-flex-grow tw-flex tw-flex-col tw--mx-6 tw-px-7 tw-py-4">
<div className="tw-flex-grow tw-flex tw-flex-col tw-bg-white tw-shadow tw-rounded-md tw-w-full">
{activeTab === 1 && (
<>
<div className="tw-grid tw-grid-cols-4 tw-gap-4 tw-w-full">
<div className="tw-col-span-full tw--ml-5">
<Description
description={description}
entityFieldTasks={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldTaskCount
)}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldThreadCount
)}
entityFqn={pipelineFQN}
entityName={entityName}
entityType={EntityType.PIPELINE}
hasEditAccess={
pipelinePermissions.EditAll ||
pipelinePermissions.EditDescription
}
isEdit={isEdit}
isReadOnly={deleted}
owner={owner}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
onEntityFieldSelect={onEntityFieldSelect}
onThreadLinkSelect={onThreadLinkSelect}
/>
</div>
</div>
<hr className="tw-my-3" />
<div
className="tw-flex-grow tw-w-full tw-h-full"
style={{ height: 'calc(100% - 250px)' }}>
{!isEmpty(tasks) ? (
<Tabs activeKey={tab} className="h-full" onChange={handleTabChange}>
<Tabs.TabPane
key={PIPELINE_DETAILS_TABS.Tasks}
tab={
<span data-testid={PIPELINE_DETAILS_TABS.Tasks}>
{t('label.tasks')}
</span>
}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Description
description={description}
entityFieldTasks={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldTaskCount
)}
entityFieldThreads={getEntityFieldThreadCounts(
EntityField.DESCRIPTION,
entityFieldThreadCount
)}
entityFqn={pipelineFQN}
entityName={entityName}
entityType={EntityType.PIPELINE}
hasEditAccess={
pipelinePermissions.EditAll ||
pipelinePermissions.EditDescription
}
isEdit={isEdit}
isReadOnly={deleted}
owner={owner}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
onEntityFieldSelect={onEntityFieldSelect}
onThreadLinkSelect={onThreadLinkSelect}
/>
</Col>
<Col span={24}>
<Table
bordered
columns={taskColumns}
dataSource={tasksInternal}
pagination={false}
rowKey="name"
size="small"
/>
</Col>
{!isEmpty(tasks) ? (
<Col span={24}>
<Card title={t('label.dag-view')}>
<div className="h-100">
<TasksDAGView
selectedExec={selectedExecution}
tasks={tasks}
/>
) : (
<div
className="tw-mt-4 tw-ml-4 tw-flex tw-justify-center tw-font-medium tw-items-center tw-border tw-border-main tw-rounded-md tw-p-8"
data-testid="no-tasks-data">
<span>No task data is available</span>
</div>
)}
</div>
</>
)}
{activeTab === 2 && (
</div>
</Card>
</Col>
) : (
<div
className="tw-py-4 tw-px-7 tw-grid tw-grid-cols-3 entity-feed-list tw--mx-7 tw--my-4"
id="activityfeed">
<div />
<ActivityFeedList
isEntityFeed
withSidePanel
className=""
deletePostHandler={deletePostHandler}
entityName={entityName}
feedList={entityThread}
postFeedHandler={postFeedHandler}
updateThreadHandler={updateThreadHandler}
onFeedFiltersUpdate={handleFeedFilterChange}
/>
<div />
className="tw-mt-4 tw-ml-4 tw-flex tw-justify-center tw-font-medium tw-items-center tw-border tw-border-main tw-rounded-md tw-p-8"
data-testid="no-tasks-data">
<span>{t('label.no-task-available')}</span>
</div>
)}
{activeTab === 3 && <ExecutionsTab pipelineFQN={pipelineFQN} />}
{activeTab === 4 && (
<div className="h-full">
<EntityLineageComponent
addLineageHandler={addLineageHandler}
deleted={deleted}
entityLineage={entityLineage}
entityLineageHandler={entityLineageHandler}
entityType={EntityType.PIPELINE}
hasEditAccess={
pipelinePermissions.EditAll ||
pipelinePermissions.EditLineage
}
isLoading={isLineageLoading}
isNodeLoading={isNodeLoading}
lineageLeafNodes={lineageLeafNodes}
loadNodeHandler={loadNodeHandler}
removeLineageHandler={removeLineageHandler}
onFullScreenClick={handleFullScreenClick}
/>
</div>
)}
{activeTab === 5 && (
<CustomPropertyTable
entityDetails={
pipelineDetails as CustomPropertyProps['entityDetails']
}
entityType={EntityType.PIPELINE}
handleExtentionUpdate={onExtensionUpdate}
/>
)}
<div
data-testid="observer-element"
id="observer-element"
ref={elementRef as RefObject<HTMLDivElement>}>
{getLoader()}
</div>
</Row>
</Tabs.TabPane>
<Tabs.TabPane
className="h-full"
key={PIPELINE_DETAILS_TABS.ActivityFeedsAndTasks}
tab={
<span data-testid={PIPELINE_DETAILS_TABS.ActivityFeedsAndTasks}>
{t('label.activity-feed-and-task-plural')}{' '}
{getCountBadge(
feedCount,
'',
PIPELINE_DETAILS_TABS.ActivityFeedsAndTasks === tab
)}
</span>
}>
<Card className="h-min-full">
<Row justify="center">
<Col span={18}>
<div id="activityfeed">
<ActivityFeedList
isEntityFeed
withSidePanel
deletePostHandler={deletePostHandler}
entityName={entityName}
feedList={entityThreads}
postFeedHandler={postFeedHandler}
updateThreadHandler={updateThreadHandler}
onFeedFiltersUpdate={handleFeedFilterChange}
/>
<div
data-testid="observer-element"
id="observer-element"
ref={elementRef as RefObject<HTMLDivElement>}>
{getLoader()}
</div>
</div>
</Col>
</Row>
</Card>
</Tabs.TabPane>
<Tabs.TabPane
key={PIPELINE_DETAILS_TABS.Executions}
tab={
<span data-testid={PIPELINE_DETAILS_TABS.Tasks}>
{t('label.executions')}
</span>
}>
<ExecutionsTab pipelineFQN={pipelineFQN} />
</Tabs.TabPane>
<Tabs.TabPane
key={PIPELINE_DETAILS_TABS.EntityLineage}
tab={
<span data-testid="Lineage">{t('label.entity-lineage')}</span>
}>
<div className="h-full bg-white">
<EntityLineageComponent
addLineageHandler={addLineageHandler}
deleted={deleted}
entityLineage={entityLineage}
entityLineageHandler={entityLineageHandler}
entityType={EntityType.PIPELINE}
hasEditAccess={
pipelinePermissions.EditAll || pipelinePermissions.EditLineage
}
isLoading={lineageLoading}
isNodeLoading={isNodeLoading}
lineageLeafNodes={lineageLeafNodes}
loadNodeHandler={loadNodeHandler}
removeLineageHandler={removeLineageHandler}
onFullScreenClick={handleFullScreenClick}
/>
</div>
</div>
</div>
</Tabs.TabPane>
<Tabs.TabPane
key={PIPELINE_DETAILS_TABS.CustomProperties}
tab={
<span data-testid="Custom Properties">
{t('label.custom-properties')}
</span>
}>
<CustomPropertyTable
entityDetails={
pipelineDetails as CustomPropertyProps['entityDetails']
}
entityType={EntityType.PIPELINE}
handleExtensionUpdate={onExtensionUpdate}
/>
</Tabs.TabPane>
<Tabs.TabPane key="*" tab="">
<Redirect to={ROUTES.NOT_FOUND} />
</Tabs.TabPane>
</Tabs>
</div>
{editTask && (
<ModalWithMarkdownEditor
header={`Edit Task: "${editTask.task.displayName}"`}
placeholder="Enter Task Description"
header={`${t('label.edit-task')}: "${
editTask.task.displayName || editTask.task.name
}"`}
placeholder={t('label.type-field-name', {
fieldName: t('label.description'),
})}
value={editTask.task.description || ''}
onCancel={closeEditTaskModal}
onSave={onTaskUpdate}
/>
)}
{threadLink ? (
<ActivityThreadPanel
createThread={createThread}
@ -606,7 +1007,7 @@ const PipelineDetails = ({
<RequestDescriptionModal
createThread={createThread}
defaultValue={getDefaultValue(owner as EntityReference)}
header="Request description"
header={t('label.request-description')}
threadLink={getEntityFeedLink(
EntityType.PIPELINE,
pipelineFQN,

View File

@ -12,59 +12,26 @@
*/
import { Operation } from 'fast-json-patch';
import {
EntityFieldThreadCount,
EntityTags,
LeafNodes,
LineagePos,
LoadingNodeState,
} from 'Models';
import { FeedFilter } from '../../enums/mydata.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
import { LeafNodes, LineagePos, LoadingNodeState } from 'Models';
import { Pipeline, Task } from '../../generated/entity/data/pipeline';
import { Thread, ThreadType } from '../../generated/entity/feed/thread';
import { EntityLineage } from '../../generated/type/entityLineage';
import { EntityReference } from '../../generated/type/entityReference';
import { Paging } from '../../generated/type/paging';
import { TagLabel } from '../../generated/type/tagLabel';
import { ThreadUpdatedFunc } from '../../interface/feed.interface';
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
import { Edge, EdgeData } from '../EntityLineage/EntityLineage.interface';
export interface PipeLineDetailsProp {
pipelineFQN: string;
version: string;
isNodeLoading: LoadingNodeState;
lineageLeafNodes: LeafNodes;
serviceType: string;
pipelineUrl: string;
entityName: string;
pipelineDetails: Pipeline;
activeTab: number;
owner: EntityReference;
description: string;
tier: TagLabel;
followers: Array<EntityReference>;
pipelineTags: Array<EntityTags>;
slashedPipelineName: TitleBreadcrumbProps['titleLinks'];
entityLineage: EntityLineage;
tasks: Task[];
deleted?: boolean;
isLineageLoading?: boolean;
entityThread: Thread[];
isentityThreadLoading: boolean;
feedCount: number;
entityFieldThreadCount: EntityFieldThreadCount[];
entityFieldTaskCount: EntityFieldThreadCount[];
paging: Paging;
pipelineStatus: Pipeline['pipelineStatus'];
fetchFeedHandler: (
after?: string,
feedFilter?: FeedFilter,
threadType?: ThreadType
) => void;
createThread: (data: CreateThread) => void;
setActiveTabHandler: (value: number) => void;
followPipelineHandler: () => void;
unfollowPipelineHandler: () => void;
settingsUpdateHandler: (updatedPipeline: Pipeline) => Promise<void>;
@ -76,12 +43,5 @@ export interface PipeLineDetailsProp {
addLineageHandler: (edge: Edge) => Promise<void>;
removeLineageHandler: (data: EdgeData) => void;
entityLineageHandler: (lineage: EntityLineage) => void;
postFeedHandler: (value: string, id: string) => void;
deletePostHandler: (
threadId: string,
postId: string,
isThread: boolean
) => void;
updateThreadHandler: ThreadUpdatedFunc;
onExtensionUpdate: (updatedPipeline: Pipeline) => Promise<void>;
}

View File

@ -11,10 +11,16 @@
* limitations under the License.
*/
import { findByTestId, findByText, render } from '@testing-library/react';
import {
findByTestId,
findByText,
fireEvent,
render,
} from '@testing-library/react';
import { LeafNodes, LoadingNodeState } from 'Models';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { act } from 'react-test-renderer';
import { Pipeline } from '../../generated/entity/data/pipeline';
import { EntityLineage } from '../../generated/type/entityLineage';
import { EntityReference } from '../../generated/type/entityReference';
@ -189,7 +195,6 @@ jest.mock('../common/CustomPropertyTable/CustomPropertyTable', () => ({
jest.mock('../../utils/CommonUtils', () => ({
addToRecentViewed: jest.fn(),
getCountBadge: jest.fn(),
getCurrentUserId: jest.fn().mockReturnValue('CurrentUserId'),
getPartialNameFromFQN: jest.fn().mockReturnValue('PartialNameFromFQN'),
getUserTeams: () => mockUserTeam,
@ -197,11 +202,13 @@ jest.mock('../../utils/CommonUtils', () => ({
getEntityPlaceHolder: jest.fn().mockReturnValue('value'),
getEntityName: jest.fn().mockReturnValue('entityName'),
getOwnerValue: jest.fn().mockReturnValue('Owner'),
getFeedCounts: jest.fn(),
getCountBadge: jest.fn().mockImplementation((count) => <p>{count}</p>),
}));
jest.mock('', () => ({
ExecutionsTab: jest.fn().mockImplementation(() => <p>Executions</p>),
}));
jest.mock('../Execution/Execution.component', () => {
return jest.fn().mockImplementation(() => <p>Executions</p>);
});
describe('Test PipelineDetails component', () => {
it('Checks if the PipelineDetails component has all the proper components rendered', async () => {
@ -213,20 +220,28 @@ describe('Test PipelineDetails component', () => {
);
const EntityPageInfo = await findByText(container, /EntityPageInfo/i);
const description = await findByText(container, /Description Component/i);
const tabs = await findByTestId(container, 'tabs');
const detailsTab = await findByTestId(tabs, 'Details');
const activityFeedTab = await findByTestId(tabs, 'Activity Feeds & Tasks');
const lineageTab = await findByTestId(tabs, 'Lineage');
const tasksTab = await findByText(container, 'label.tasks');
const activityFeedTab = await findByText(
container,
'label.activity-feed-and-task-plural'
);
const lineageTab = await findByText(container, 'label.entity-lineage');
const executionsTab = await findByText(container, 'label.executions');
const customPropertiesTab = await findByText(
container,
'label.custom-properties'
);
expect(EntityPageInfo).toBeInTheDocument();
expect(description).toBeInTheDocument();
expect(tabs).toBeInTheDocument();
expect(detailsTab).toBeInTheDocument();
expect(tasksTab).toBeInTheDocument();
expect(activityFeedTab).toBeInTheDocument();
expect(lineageTab).toBeInTheDocument();
expect(executionsTab).toBeInTheDocument();
expect(customPropertiesTab).toBeInTheDocument();
});
it('Check if active tab is details', async () => {
it('Check if active tab is tasks', async () => {
const { container } = render(
<PipelineDetails {...PipelineDetailsProps} />,
{
@ -251,23 +266,39 @@ describe('Test PipelineDetails component', () => {
it('Check if active tab is activity feed', async () => {
const { container } = render(
<PipelineDetails {...PipelineDetailsProps} activeTab={2} />,
<PipelineDetails {...PipelineDetailsProps} />,
{
wrapper: MemoryRouter,
}
);
const activityFeedTab = await findByText(
container,
'label.activity-feed-and-task-plural'
);
await act(async () => {
fireEvent.click(activityFeedTab);
});
const activityFeedList = await findByText(container, /ActivityFeedList/i);
expect(activityFeedList).toBeInTheDocument();
});
it('should render execution tab if active tab is 3', async () => {
it('should render execution tab', async () => {
const { container } = render(
<PipelineDetails {...PipelineDetailsProps} activeTab={3} />,
<PipelineDetails {...PipelineDetailsProps} />,
{
wrapper: MemoryRouter,
}
);
const activityFeedTab = await findByText(container, 'label.executions');
await act(async () => {
fireEvent.click(activityFeedTab);
});
const executions = await findByText(container, 'Executions');
expect(executions).toBeInTheDocument();
@ -275,11 +306,16 @@ describe('Test PipelineDetails component', () => {
it('Check if active tab is lineage', async () => {
const { container } = render(
<PipelineDetails {...PipelineDetailsProps} activeTab={4} />,
<PipelineDetails {...PipelineDetailsProps} />,
{
wrapper: MemoryRouter,
}
);
const activityFeedTab = await findByText(container, 'label.entity-lineage');
await act(async () => {
fireEvent.click(activityFeedTab);
});
const lineage = await findByTestId(container, 'lineage');
expect(lineage).toBeInTheDocument();
@ -287,11 +323,20 @@ describe('Test PipelineDetails component', () => {
it('Check if active tab is custom properties', async () => {
const { container } = render(
<PipelineDetails {...PipelineDetailsProps} activeTab={5} />,
<PipelineDetails {...PipelineDetailsProps} />,
{
wrapper: MemoryRouter,
}
);
const activityFeedTab = await findByText(
container,
'label.custom-properties'
);
await act(async () => {
fireEvent.click(activityFeedTab);
});
const customProperties = await findByText(
container,
'CustomPropertyTable.component'
@ -302,16 +347,23 @@ describe('Test PipelineDetails component', () => {
it('Should create an observer if IntersectionObserver is available', async () => {
const { container } = render(
<PipelineDetails {...PipelineDetailsProps} activeTab={5} />,
<PipelineDetails {...PipelineDetailsProps} />,
{
wrapper: MemoryRouter,
}
);
const activityFeedTab = await findByText(
container,
'label.activity-feed-and-task-plural'
);
await act(async () => {
fireEvent.click(activityFeedTab);
});
const obServerElement = await findByTestId(container, 'observer-element');
expect(obServerElement).toBeInTheDocument();
expect(mockObserve).toHaveBeenCalled();
});
});

View File

@ -609,7 +609,7 @@ const TopicDetails: React.FC<TopicDetailsProps> = ({
topicDetails as CustomPropertyProps['entityDetails']
}
entityType={EntityType.TOPIC}
handleExtentionUpdate={onExtensionUpdate}
handleExtensionUpdate={onExtensionUpdate}
/>
)}
<div

View File

@ -23,5 +23,5 @@ export type EntityDetails = Table & Topic & Dashboard & Pipeline & Mlmodel;
export interface CustomPropertyProps {
entityDetails: EntityDetails;
entityType: EntityType;
handleExtentionUpdate: (updatedTable: EntityDetails) => Promise<void>;
handleExtensionUpdate: (updatedTable: EntityDetails) => Promise<void>;
}

View File

@ -63,11 +63,11 @@ jest.mock('../../../axiosAPIs/metadataTypeAPI', () => ({
}));
const mockTableDetails = {} as Table & Topic & Dashboard & Pipeline & Mlmodel;
const handleExtentionUpdate = jest.fn();
const handleExtensionUpdate = jest.fn();
const mockProp = {
entityDetails: mockTableDetails,
handleExtentionUpdate,
handleExtensionUpdate,
entityType: EntityType.TABLE,
};

View File

@ -25,7 +25,7 @@ import { PropertyValue } from './PropertyValue';
export const CustomPropertyTable: FC<CustomPropertyProps> = ({
entityDetails,
handleExtentionUpdate,
handleExtensionUpdate,
entityType,
}) => {
const [entityTypeDetail, setEntityTypeDetail] = useState<Type>({} as Type);
@ -41,7 +41,7 @@ export const CustomPropertyTable: FC<CustomPropertyProps> = ({
const onExtensionUpdate = async (
updatedExtension: CustomPropertyProps['entityDetails']['extension']
) => {
await handleExtentionUpdate({
await handleExtensionUpdate({
...entityDetails,
extension: updatedExtension,
});

View File

@ -15,7 +15,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { EntityTags, TagOption } from 'Models';
import React, { Fragment, FunctionComponent, useEffect, useState } from 'react';
import React, {
Fragment,
FunctionComponent,
useCallback,
useEffect,
useState,
} from 'react';
import AsyncSelect from 'react-select/async';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { TagSource } from '../../generated/type/tagLabel';
@ -87,11 +93,14 @@ const TagsContainer: FunctionComponent<TagsContainerProps> = ({
setTags(updatedTags);
};
const handleSave = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
onSelectionChange(tags);
};
const handleSave = useCallback(
(event: React.MouseEvent<HTMLElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
onSelectionChange(tags);
},
[tags]
);
const handleCancel = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
event.preventDefault();

View File

@ -0,0 +1,20 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum PIPELINE_DETAILS_TABS {
Tasks = 'tasks',
ActivityFeedsAndTasks = 'activity-feeds-tasks',
Executions = 'executions',
EntityLineage = 'entity-lineage',
CustomProperties = 'custom-properties',
}

View File

@ -212,6 +212,8 @@
"select-rule-effect": "Select Rule Effect",
"field-required": "{{field}} is required",
"field-required-plural": "{{field}} are required",
"edit-task": "Edit Task",
"type-filed-name": "Type {{fieldName}}",
"no-execution-runs-found": "No execution runs found for the pipeline.",
"last-no-of-days": "Last {{day}} Days",
"tier-number": "Tier{{tier}}",
@ -224,6 +226,12 @@
"pipeline": "Pipeline",
"function": "Function",
"edge-information": "Edge Information",
"tasks": "Tasks",
"activity-feed-and-task-plural": "Activity Feeds & tasks",
"executions": "Executions",
"entity-lineage": "Entity Lineage",
"custom-properties": "Custom Properties",
"dag-view": "DAG view",
"read-more": "read more",
"read-less": "read less",
"no-owner": "No Owner",
@ -269,6 +277,7 @@
"server": {
"no-followed-entities": "You have not followed anything yet.",
"no-owned-entities": "You have not owned anything yet.",
"no-task-available": "No task data is available",
"entity-fetch-error": "Error while fetching {{entity}}",
"feed-post-error": "Error while posting the message!",
"unexpected-error": "An unexpected error occurred.",

View File

@ -852,7 +852,7 @@ const DatasetDetailsPage: FunctionComponent = () => {
fetchFeedHandler={handleFeedFetchFromFeedList}
followTableHandler={followTable}
followers={followers}
handleExtentionUpdate={handleExtentionUpdate}
handleExtensionUpdate={handleExtentionUpdate}
isLineageLoading={isLineageLoading}
isNodeLoading={isNodeLoading}
isQueriesLoading={isTableQueriesLoading}

View File

@ -42,6 +42,7 @@ import {
getTableTabPath,
getTopicDetailsPath,
} from '../../constants/constants';
import { PIPELINE_DETAILS_TABS } from '../../constants/pipeline.constants';
import { EntityType, FqnPart } from '../../enums/entity.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { Dashboard } from '../../generated/entity/data/dashboard';
@ -202,7 +203,10 @@ const LineagePage = () => {
const pipelineRes = await getPipelineByFqn(entityFQN, '');
updateBreadcrumb(
pipelineRes,
getPipelineDetailsPath(entityFQN, 'lineage')
getPipelineDetailsPath(
entityFQN,
PIPELINE_DETAILS_TABS.EntityLineage
)
);
}

View File

@ -13,23 +13,11 @@
import { AxiosError } from 'axios';
import { compare, Operation } from 'fast-json-patch';
import { isEmpty, isUndefined, omitBy } from 'lodash';
import { isUndefined, omitBy } from 'lodash';
import { observer } from 'mobx-react';
import {
EntityFieldThreadCount,
EntityTags,
LeafNodes,
LineagePos,
LoadingNodeState,
} from 'Models';
import React, { useEffect, useState } from 'react';
import { LeafNodes, LineagePos, LoadingNodeState } from 'Models';
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import AppState from '../../AppState';
import {
getAllFeeds,
postFeedById,
postThread,
} from '../../axiosAPIs/feedsAPI';
import { getLineageByFQN } from '../../axiosAPIs/lineageAPI';
import { addLineage, deleteLineageEdge } from '../../axiosAPIs/miscAPI';
import {
@ -50,65 +38,45 @@ import { usePermissionProvider } from '../../components/PermissionProvider/Permi
import { ResourceEntity } from '../../components/PermissionProvider/PermissionProvider.interface';
import PipelineDetails from '../../components/PipelineDetails/PipelineDetails.component';
import {
getPipelineDetailsPath,
getServiceDetailsPath,
getVersionPath,
} from '../../constants/constants';
import { NO_PERMISSION_TO_VIEW } from '../../constants/HelperTextUtil';
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
import { FeedFilter } from '../../enums/mydata.enum';
import { EntityType } from '../../enums/entity.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { CreateThread } from '../../generated/api/feed/createThread';
import { Pipeline, Task } from '../../generated/entity/data/pipeline';
import { Post, Thread, ThreadType } from '../../generated/entity/feed/thread';
import { Connection } from '../../generated/entity/services/dashboardService';
import { EntityLineage } from '../../generated/type/entityLineage';
import { EntityReference } from '../../generated/type/entityReference';
import { Paging } from '../../generated/type/paging';
import { TagLabel } from '../../generated/type/tagLabel';
import jsonData from '../../jsons/en';
import {
addToRecentViewed,
getCurrentUserId,
getEntityMissingError,
getEntityName,
getFeedCounts,
} from '../../utils/CommonUtils';
import { getEntityFeedLink, getEntityLineage } from '../../utils/EntityUtils';
import { deletePost, updateThreadData } from '../../utils/FeedUtils';
import { getEntityLineage } from '../../utils/EntityUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import {
defaultFields,
getCurrentPipelineTab,
pipelineDetailsTabs,
} from '../../utils/PipelineDetailsUtils';
import { defaultFields } from '../../utils/PipelineDetailsUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { getTagsWithoutTier, getTierTags } from '../../utils/TableUtils';
import { showErrorToast } from '../../utils/ToastUtils';
const PipelineDetailsPage = () => {
const USERId = getCurrentUserId();
const history = useHistory();
const { pipelineFQN, tab } = useParams() as Record<string, string>;
const { pipelineFQN } = useParams<{ pipelineFQN: string }>();
const [pipelineDetails, setPipelineDetails] = useState<Pipeline>(
{} as Pipeline
);
const [pipelineId, setPipelineId] = useState<string>('');
const [isLoading, setLoading] = useState<boolean>(true);
const [isLineageLoading, setIsLineageLoading] = useState<boolean>(false);
const [description, setDescription] = useState<string>('');
const [followers, setFollowers] = useState<Array<EntityReference>>([]);
const [owner, setOwner] = useState<EntityReference>();
const [tier, setTier] = useState<TagLabel>();
const [tags, setTags] = useState<Array<EntityTags>>([]);
const [activeTab, setActiveTab] = useState<number>(
getCurrentPipelineTab(tab)
);
const [tasks, setTasks] = useState<Task[]>([]);
const [pipelineUrl, setPipelineUrl] = useState<string>('');
const [displayName, setDisplayName] = useState<string>('');
const [serviceType, setServiceType] = useState<string>('');
const [slashedPipelineName, setSlashedPipelineName] = useState<
TitleBreadcrumbProps['titleLinks']
>([]);
@ -122,24 +90,9 @@ const PipelineDetailsPage = () => {
);
const [leafNodes, setLeafNodes] = useState<LeafNodes>({} as LeafNodes);
const [currentVersion, setCurrentVersion] = useState<string>();
const [deleted, setDeleted] = useState<boolean>(false);
const [isError, setIsError] = useState(false);
const [entityThread, setEntityThread] = useState<Thread[]>([]);
const [isentityThreadLoading, setIsentityThreadLoading] =
useState<boolean>(false);
const [feedCount, setFeedCount] = useState<number>(0);
const [entityFieldThreadCount, setEntityFieldThreadCount] = useState<
EntityFieldThreadCount[]
>([]);
const [paging, setPaging] = useState<Paging>({} as Paging);
const [pipeLineStatus, setPipelineStatus] =
useState<Pipeline['pipelineStatus']>();
const [entityFieldTaskCount, setEntityFieldTaskCount] = useState<
EntityFieldThreadCount[]
>([]);
const [paging] = useState<Paging>({} as Paging);
const [pipelinePermissions, setPipelinePermissions] = useState(
DEFAULT_ENTITY_PERMISSION
@ -164,30 +117,12 @@ const PipelineDetailsPage = () => {
}
};
const activeTabHandler = (tabValue: number) => {
const currentTabIndex = tabValue - 1;
if (pipelineDetailsTabs[currentTabIndex].path !== tab) {
setActiveTab(
getCurrentPipelineTab(pipelineDetailsTabs[currentTabIndex].path)
);
history.push({
pathname: getPipelineDetailsPath(
pipelineFQN,
pipelineDetailsTabs[currentTabIndex].path
),
});
}
};
const getEntityFeedCount = () => {
getFeedCounts(
EntityType.PIPELINE,
pipelineFQN,
setEntityFieldThreadCount,
setEntityFieldTaskCount,
setFeedCount
);
};
const { pipelineId, currentVersion } = useMemo(() => {
return {
pipelineId: pipelineDetails.id,
currentVersion: pipelineDetails.version + '',
};
}, [pipelineDetails]);
const saveUpdatedPipelineData = (updatedData: Pipeline) => {
const jsonPatch = compare(
@ -198,70 +133,6 @@ const PipelineDetailsPage = () => {
return patchPipelineDetails(pipelineId, jsonPatch);
};
const getLineageData = () => {
setIsLineageLoading(true);
getLineageByFQN(pipelineFQN, EntityType.PIPELINE)
.then((res) => {
if (res) {
setEntityLineage(res);
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['fetch-lineage-error']
);
})
.finally(() => {
setIsLineageLoading(false);
});
};
const getFeedData = (
after?: string,
feedFilter?: FeedFilter,
threadType?: ThreadType
) => {
setIsentityThreadLoading(true);
getAllFeeds(
getEntityFeedLink(EntityType.PIPELINE, pipelineFQN),
after,
threadType,
feedFilter,
undefined,
USERId
)
.then((res) => {
const { data, paging: pagingObj } = res;
if (data) {
setPaging(pagingObj);
setEntityThread((prevData) => [...prevData, ...data]);
} else {
showErrorToast(
jsonData['api-error-messages']['fetch-entity-feed-error']
);
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['fetch-entity-feed-error']
);
})
.finally(() => setIsentityThreadLoading(false));
};
const handleFeedFetchFromFeedList = (
after?: string,
filterType?: FeedFilter,
type?: ThreadType
) => {
!after && setEntityThread([]);
getFeedData(after, filterType, type);
};
const fetchServiceDetails = (type: string, fqn: string) => {
return new Promise<string>((resolve, reject) => {
getServiceByFQN(type + 's', fqn, ['owner'])
@ -291,32 +162,16 @@ const PipelineDetailsPage = () => {
if (res) {
const {
id,
deleted,
description,
followers = [],
fullyQualifiedName,
service,
serviceType,
tags = [],
owner,
displayName,
name,
tasks,
pipelineUrl = '',
pipelineStatus,
version,
} = res;
setDisplayName(displayName || name);
setPipelineDetails(res);
setCurrentVersion(version + '');
setPipelineId(id);
setDescription(description ?? '');
setFollowers(followers);
setOwner(owner);
setTier(getTierTags(tags));
setTags(getTagsWithoutTier(tags));
setServiceType(serviceType ?? '');
setDeleted(Boolean(deleted));
const serviceName = service.name ?? '';
setSlashedPipelineName([
{
@ -345,11 +200,6 @@ const PipelineDetailsPage = () => {
id: id,
});
setPipelineUrl(pipelineUrl);
setTasks(tasks || []);
setPipelineStatus(pipelineStatus as Pipeline['pipelineStatus']);
fetchServiceDetails(service.type, service.name ?? '')
.then((hostPort: string) => {
setPipelineUrl(hostPort + pipelineUrl);
@ -384,30 +234,6 @@ const PipelineDetailsPage = () => {
});
};
const fetchTabSpecificData = (tabField = '') => {
switch (tabField) {
case TabSpecificField.LINEAGE: {
if (!deleted) {
if (isEmpty(entityLineage)) {
getLineageData();
}
break;
}
break;
}
case TabSpecificField.ACTIVITY_FEED: {
getFeedData();
break;
}
default:
break;
}
};
const followPipeline = () => {
addFollower(pipelineId, USERId)
.then((res) => {
@ -452,11 +278,7 @@ const PipelineDetailsPage = () => {
try {
const response = await saveUpdatedPipelineData(updatedPipeline);
if (response) {
const { description = '', version } = response;
setCurrentVersion(version + '');
setPipelineDetails(response);
setDescription(description);
getEntityFeedCount();
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
@ -471,10 +293,7 @@ const PipelineDetailsPage = () => {
.then((res) => {
if (res) {
setPipelineDetails({ ...res, tags: res.tags ?? [] });
setCurrentVersion(res.version + '');
setOwner(res.owner);
setTier(getTierTags(res.tags ?? []));
getEntityFeedCount();
resolve();
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
@ -495,10 +314,6 @@ const PipelineDetailsPage = () => {
.then((res) => {
if (res) {
setPipelineDetails(res);
setTier(getTierTags(res.tags ?? []));
setCurrentVersion(res.version + '');
setTags(getTagsWithoutTier(res.tags ?? []));
getEntityFeedCount();
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
@ -517,7 +332,6 @@ const PipelineDetailsPage = () => {
if (response) {
setTasks(response.tasks || []);
getEntityFeedCount();
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
@ -605,83 +419,12 @@ const PipelineDetailsPage = () => {
});
};
const postFeedHandler = (value: string, id: string) => {
const currentUser = AppState.userDetails?.name ?? AppState.users[0]?.name;
const data = {
message: value,
from: currentUser,
} as Post;
postFeedById(id, data)
.then((res) => {
if (res) {
const { id, posts } = res;
setEntityThread((pre) => {
return pre.map((thread) => {
if (thread.id === id) {
return { ...res, posts: posts?.slice(-3) };
} else {
return thread;
}
});
});
getEntityFeedCount();
} else {
throw jsonData['api-error-messages']['unexpected-server-response'];
}
})
.catch((err: AxiosError) => {
showErrorToast(err, jsonData['api-error-messages']['add-feed-error']);
});
};
const createThread = (data: CreateThread) => {
postThread(data)
.then((res) => {
if (res) {
setEntityThread((pre) => [...pre, res]);
getEntityFeedCount();
} else {
showErrorToast(
jsonData['api-error-messages']['unexpected-server-response']
);
}
})
.catch((err: AxiosError) => {
showErrorToast(
err,
jsonData['api-error-messages']['create-conversation-error']
);
});
};
const deletePostHandler = (
threadId: string,
postId: string,
isThread: boolean
) => {
deletePost(threadId, postId, isThread, setEntityThread);
};
const updateThreadHandler = (
threadId: string,
postId: string,
isThread: boolean,
data: Operation[]
) => {
updateThreadData(threadId, postId, isThread, data, setEntityThread);
};
const handleExtentionUpdate = async (updatedPipeline: Pipeline) => {
const handleExtensionUpdate = async (updatedPipeline: Pipeline) => {
try {
const data = await saveUpdatedPipelineData(updatedPipeline);
if (data) {
const { version, owner: ownerValue, tags = [] } = data;
setCurrentVersion(version + '');
setPipelineDetails(data);
setOwner(ownerValue);
setTier(getTierTags(tags));
} else {
throw jsonData['api-error-messages']['update-entity-error'];
}
@ -693,15 +436,10 @@ const PipelineDetailsPage = () => {
}
};
useEffect(() => {
fetchTabSpecificData(pipelineDetailsTabs[activeTab - 1].field);
}, [activeTab]);
useEffect(() => {
if (pipelinePermissions.ViewAll || pipelinePermissions.ViewBasic) {
fetchPipelineDetail(pipelineFQN);
setEntityLineage({} as EntityLineage);
getEntityFeedCount();
}
}, [pipelinePermissions, pipelineFQN]);
@ -709,13 +447,6 @@ const PipelineDetailsPage = () => {
fetchResourcePermission(pipelineFQN);
}, [pipelineFQN]);
useEffect(() => {
if (pipelineDetailsTabs[activeTab - 1].path !== tab) {
setActiveTab(getCurrentPipelineTab(tab));
}
setEntityThread([]);
}, [tab]);
return (
<>
{isLoading ? (
@ -728,50 +459,29 @@ const PipelineDetailsPage = () => {
<>
{pipelinePermissions.ViewAll || pipelinePermissions.ViewBasic ? (
<PipelineDetails
activeTab={activeTab}
addLineageHandler={addLineageHandler}
createThread={createThread}
deletePostHandler={deletePostHandler}
deleted={deleted}
description={description}
descriptionUpdateHandler={descriptionUpdateHandler}
entityFieldTaskCount={entityFieldTaskCount}
entityFieldThreadCount={entityFieldThreadCount}
entityLineage={entityLineage}
entityLineageHandler={entityLineageHandler}
entityName={displayName}
entityThread={entityThread}
feedCount={feedCount}
fetchFeedHandler={handleFeedFetchFromFeedList}
followPipelineHandler={followPipeline}
followers={followers}
isLineageLoading={isLineageLoading}
isNodeLoading={isNodeLoading}
isentityThreadLoading={isentityThreadLoading}
lineageLeafNodes={leafNodes}
loadNodeHandler={loadNodeHandler}
owner={owner as EntityReference}
paging={paging}
pipelineDetails={pipelineDetails}
pipelineFQN={pipelineFQN}
pipelineStatus={pipeLineStatus}
pipelineTags={tags}
pipelineUrl={pipelineUrl}
postFeedHandler={postFeedHandler}
removeLineageHandler={removeLineageHandler}
serviceType={serviceType}
setActiveTabHandler={activeTabHandler}
settingsUpdateHandler={settingsUpdateHandler}
slashedPipelineName={slashedPipelineName}
tagUpdateHandler={onTagUpdate}
taskUpdateHandler={onTaskUpdate}
tasks={tasks}
tier={tier as TagLabel}
unfollowPipelineHandler={unfollowPipeline}
updateThreadHandler={updateThreadHandler}
version={currentVersion as string}
versionHandler={versionHandler}
onExtensionUpdate={handleExtentionUpdate}
onExtensionUpdate={handleExtensionUpdate}
/>
) : (
<ErrorPlaceHolder>{NO_PERMISSION_TO_VIEW}</ErrorPlaceHolder>

View File

@ -190,7 +190,7 @@ const TourPage = () => {
fetchFeedHandler={handleCountChange}
followTableHandler={handleCountChange}
followers={mockDatasetData.followers}
handleExtentionUpdate={handleCountChange}
handleExtensionUpdate={handleCountChange}
isNodeLoading={{
id: undefined,
state: false,

View File

@ -142,6 +142,9 @@
.h-9 {
height: 36px;
}
.h-100 {
height: 400px;
}
.h-min-100 {
min-height: 100vh;
}
@ -156,6 +159,10 @@
height: 100%;
}
.h-min-full {
min-height: 100%;
}
// Text alignment
.text-left {
text-align: left;
@ -280,6 +287,9 @@
text-decoration: none;
}
.bg-white {
background: @white;
}
.font-semibold {
font-weight: 600;
}

View File

@ -14,3 +14,9 @@
.ant-tabs-tab.ant-tabs-tab-active {
font-weight: 500;
}
.ant-tabs.ant-tabs-top.h-full {
.ant-tabs-content.ant-tabs-content-top {
height: 100%;
}
}

View File

@ -194,6 +194,9 @@
.mr-8 {
margin-right: 2rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
@ -231,9 +234,6 @@
.m-52 {
margin: 13rem;
}
.mr-2 {
margin-right: 8px;
}
.my-4 {
margin-top: 1rem;

View File

@ -1,3 +1,16 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.ant-tree-switcher {
display: flex;
justify-content: center;
@ -42,3 +55,13 @@
.ant-tree-switcher-icon {
color: black;
}
.execution-node-container {
overflow-x: auto;
white-space: nowrap;
overflow-y: hidden;
}
.execution-node-container::-webkit-scrollbar {
display: none;
}

View File

@ -16,8 +16,10 @@ import { flatten, isEmpty } from 'lodash';
import { Bucket, EntityTags, TableColumn, TagOption } from 'Models';
import { getCategory, getTags } from '../axiosAPIs/tagAPI';
import { TAG_VIEW_CAP } from '../constants/constants';
import { SettledStatus } from '../enums/axios.enum';
import { TagCategory, TagClass } from '../generated/entity/tags/tagCategory';
import { LabelType, State, TagSource } from '../generated/type/tagLabel';
import { fetchGlossaryTerms, getGlossaryTermlist } from './GlossaryUtils';
export const getTagCategories = async (fields?: Array<string> | string) => {
try {
@ -118,3 +120,34 @@ export const getTagsWithLabel = (tags: Array<Bucket>) => {
export const getTagDisplay = (tag: string) => {
return tag.length > TAG_VIEW_CAP ? `${tag.slice(0, TAG_VIEW_CAP)}...` : tag;
};
export const fetchTagsAndGlossaryTerms = async () => {
const responses = await Promise.allSettled([
getTagCategories(),
fetchGlossaryTerms(),
]);
let tagsAndTerms: TagOption[] = [];
if (
responses[0].status === SettledStatus.FULFILLED &&
responses[0].value.data
) {
tagsAndTerms = getTaglist(responses[0].value.data).map((tag) => {
return { fqn: tag, source: 'Tag' };
});
}
if (
responses[1].status === SettledStatus.FULFILLED &&
responses[1].value &&
responses[1].value.length > 0
) {
const glossaryTerms: TagOption[] = getGlossaryTermlist(
responses[1].value
).map((tag) => {
return { fqn: tag, source: 'Glossary' };
});
tagsAndTerms = [...tagsAndTerms, ...glossaryTerms];
}
return tagsAndTerms;
};