mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-25 17:04:54 +00:00
This commit is contained in:
parent
1c6046cf8e
commit
7a517cd26d
@ -11,12 +11,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import { isNil, isUndefined } from 'lodash';
|
||||
import { EntityFieldThreads, EntityTags, ExtraInfo } from 'Models';
|
||||
import React, { Fragment, RefObject, useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { EntityTags, ExtraInfo } from 'Models';
|
||||
import React, { RefObject, useEffect, useState } from 'react';
|
||||
import AppState from '../../AppState';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
|
||||
import { getTeamAndUserDetailsPath } from '../../constants/constants';
|
||||
@ -24,7 +21,6 @@ import { observerOptions } from '../../constants/Mydata.constants';
|
||||
import { EntityType } from '../../enums/entity.enum';
|
||||
import { OwnerType } from '../../enums/user.enum';
|
||||
import { Pipeline, Task } from '../../generated/entity/data/pipeline';
|
||||
import { Operation } from '../../generated/entity/policies/accessControl/rule';
|
||||
import { EntityReference } from '../../generated/type/entityReference';
|
||||
import { Paging } from '../../generated/type/paging';
|
||||
import { LabelType, State } from '../../generated/type/tagLabel';
|
||||
@ -33,24 +29,15 @@ import {
|
||||
getCurrentUserId,
|
||||
getEntityName,
|
||||
getEntityPlaceHolder,
|
||||
getHtmlForNonAdminAction,
|
||||
isEven,
|
||||
} from '../../utils/CommonUtils';
|
||||
import { getEntityFeedLink } from '../../utils/EntityUtils';
|
||||
import {
|
||||
getDefaultValue,
|
||||
getFieldThreadElement,
|
||||
} from '../../utils/FeedElementUtils';
|
||||
import { getDefaultValue } from '../../utils/FeedElementUtils';
|
||||
import { getEntityFieldThreadCounts } from '../../utils/FeedUtils';
|
||||
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
||||
import { getTagsWithoutTier } from '../../utils/TableUtils';
|
||||
import ActivityFeedList from '../ActivityFeed/ActivityFeedList/ActivityFeedList';
|
||||
import ActivityThreadPanel from '../ActivityFeed/ActivityThreadPanel/ActivityThreadPanel';
|
||||
import Description from '../common/description/Description';
|
||||
import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo';
|
||||
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
|
||||
import PopOver from '../common/popover/PopOver';
|
||||
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
|
||||
import TabsPane from '../common/TabsPane/TabsPane';
|
||||
import PageContainer from '../containers/PageContainer';
|
||||
import Entitylineage from '../EntityLineage/EntityLineage.component';
|
||||
@ -59,6 +46,7 @@ import ManageTabComponent from '../ManageTab/ManageTab.component';
|
||||
import { ModalWithMarkdownEditor } from '../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||
import RequestDescriptionModal from '../Modals/RequestDescriptionModal/RequestDescriptionModal';
|
||||
import PipelineStatusList from '../PipelineStatusList/PipelineStatusList.component';
|
||||
import TasksDAGView from '../TasksDAGView/TasksDAGView';
|
||||
import { PipeLineDetailsProp } from './PipelineDetails.interface';
|
||||
|
||||
const PipelineDetails = ({
|
||||
@ -252,10 +240,6 @@ const PipelineDetails = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateTask = (task: Task, index: number) => {
|
||||
setEditTask({ task, index });
|
||||
};
|
||||
|
||||
const closeEditTaskModal = (): void => {
|
||||
setEditTask(undefined);
|
||||
};
|
||||
@ -384,15 +368,15 @@ const PipelineDetails = ({
|
||||
versionHandler={versionHandler}
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
/>
|
||||
<div className="tw-mt-4 tw-flex tw-flex-col tw-flex-grow">
|
||||
<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-bg-white tw-flex-grow tw-p-4 tw-shadow tw-rounded-md">
|
||||
<div className="tw-flex-grow tw-flex tw-flex-col tw--mx-6 tw-px-7 tw-py-4 tw-w-full">
|
||||
<div className="tw-flex-grow tw-flex tw-flex-col tw-bg-white tw-p-4 tw-shadow tw-rounded-md tw-w-full">
|
||||
{activeTab === 1 && (
|
||||
<>
|
||||
<div className="tw-grid tw-grid-cols-4 tw-gap-4 tw-w-full">
|
||||
@ -418,135 +402,9 @@ const PipelineDetails = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tw-table-responsive tw-my-6">
|
||||
<div className="tw-flex-grow tw-w-full tw-h-full">
|
||||
{tasks ? (
|
||||
<table className="tw-w-full" data-testid="tasks-table">
|
||||
<thead>
|
||||
<tr className="tableHead-row">
|
||||
<th className="tableHead-cell">Task Name</th>
|
||||
<th className="tableHead-cell">Description</th>
|
||||
<th className="tableHead-cell">Task Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="tableBody">
|
||||
{tasks?.map((task, index) => (
|
||||
<tr
|
||||
className={classNames(
|
||||
'tableBody-row',
|
||||
!isEven(index + 1) ? 'odd-row' : null
|
||||
)}
|
||||
key={index}>
|
||||
<td className="tableBody-cell">
|
||||
<Link
|
||||
target="_blank"
|
||||
to={{ pathname: task.taskUrl }}>
|
||||
<span className="tw-flex">
|
||||
<span className="tw-mr-1">
|
||||
{task.displayName}
|
||||
</span>
|
||||
<SVGIcons
|
||||
alt="external-link"
|
||||
className="tw-align-middle"
|
||||
icon="external-link"
|
||||
width="12px"
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="tw-group tableBody-cell tw-relative">
|
||||
<div
|
||||
className="tw-cursor-pointer tw-flex"
|
||||
data-testid="description">
|
||||
<div>
|
||||
{task.description ? (
|
||||
<RichTextEditorPreviewer
|
||||
markdown={task.description}
|
||||
/>
|
||||
) : (
|
||||
<span className="tw-no-description">
|
||||
No description{' '}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!deleted && (
|
||||
<Fragment>
|
||||
<NonAdminAction
|
||||
html={getHtmlForNonAdminAction(
|
||||
Boolean(owner)
|
||||
)}
|
||||
isOwner={hasEditAccess()}
|
||||
permission={Operation.UpdateDescription}
|
||||
position="top">
|
||||
<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"
|
||||
onClick={() =>
|
||||
handleUpdateTask(task, index)
|
||||
}>
|
||||
<SVGIcons
|
||||
alt="edit"
|
||||
icon="icon-edit"
|
||||
title="Edit"
|
||||
width="12px"
|
||||
/>
|
||||
</button>
|
||||
</NonAdminAction>
|
||||
{!isNil(
|
||||
getFieldThreadElement(
|
||||
task.name,
|
||||
'description',
|
||||
getEntityFieldThreadCounts(
|
||||
'tasks',
|
||||
entityFieldThreadCount
|
||||
) as EntityFieldThreads[],
|
||||
onThreadLinkSelect
|
||||
)
|
||||
) &&
|
||||
!isUndefined(onEntityFieldSelect) &&
|
||||
!task.description ? (
|
||||
<button
|
||||
className="focus:tw-outline-none tw-ml-1 tw-opacity-0 group-hover:tw-opacity-100 tw--mt-2"
|
||||
data-testid="request-description"
|
||||
onClick={() =>
|
||||
onEntityFieldSelect?.(
|
||||
`tasks/${task.name}/description`
|
||||
)
|
||||
}>
|
||||
<PopOver
|
||||
position="top"
|
||||
title="Request description"
|
||||
trigger="mouseenter">
|
||||
<SVGIcons
|
||||
alt="request-description"
|
||||
className="tw-mt-2.5"
|
||||
icon={Icons.REQUEST}
|
||||
/>
|
||||
</PopOver>
|
||||
</button>
|
||||
) : null}
|
||||
{getFieldThreadElement(
|
||||
task.name,
|
||||
'description',
|
||||
getEntityFieldThreadCounts(
|
||||
'tasks',
|
||||
entityFieldThreadCount
|
||||
) as EntityFieldThreads[],
|
||||
onThreadLinkSelect,
|
||||
EntityType.PIPELINE,
|
||||
pipelineFQN,
|
||||
`tasks/${task.name}/description`,
|
||||
Boolean(task.description)
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="tableBody-cell">
|
||||
{task.taskType}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<TasksDAGView 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">
|
||||
<span>No task data is available</span>
|
||||
|
@ -22,6 +22,15 @@ import { Paging } from '../../generated/type/paging';
|
||||
import { TagLabel } from '../../generated/type/tagLabel';
|
||||
import PipelineDetails from './PipelineDetails.component';
|
||||
|
||||
/**
|
||||
* mock implementation of ResizeObserver
|
||||
*/
|
||||
window.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../authentication/auth-provider/AuthProvider', () => {
|
||||
return {
|
||||
useAuthContext: jest.fn(() => ({
|
||||
@ -149,6 +158,10 @@ jest.mock('../PipelineStatusList/PipelineStatusList.component', () => {
|
||||
.mockReturnValue(<p data-testid="pipeline-status-list">Pipeline Status</p>);
|
||||
});
|
||||
|
||||
jest.mock('../TasksDAGView/TasksDAGView', () => {
|
||||
return jest.fn().mockReturnValue(<p data-testid="tasks-dag">Tasks DAG</p>);
|
||||
});
|
||||
|
||||
jest.mock('../../utils/CommonUtils', () => ({
|
||||
addToRecentViewed: jest.fn(),
|
||||
getCountBadge: jest.fn(),
|
||||
@ -192,7 +205,7 @@ describe('Test PipelineDetails component', () => {
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
const taskDetail = await findByTestId(container, 'tasks-table');
|
||||
const taskDetail = await findByTestId(container, 'tasks-dag');
|
||||
|
||||
expect(taskDetail).toBeInTheDocument();
|
||||
});
|
||||
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { findByTestId, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import TasksDAGView from './TasksDAGView';
|
||||
|
||||
/**
|
||||
* mock implementation of ResizeObserver
|
||||
*/
|
||||
window.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../authentication/auth-provider/AuthProvider', () => {
|
||||
return {
|
||||
useAuthContext: jest.fn(() => ({
|
||||
isAuthDisabled: false,
|
||||
isAuthenticated: true,
|
||||
isProtectedRoute: jest.fn().mockReturnValue(true),
|
||||
isTourRoute: jest.fn().mockReturnValue(false),
|
||||
onLogoutHandler: jest.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const TasksDAGViewProps = {
|
||||
tasks: [
|
||||
{
|
||||
name: 'task1',
|
||||
},
|
||||
{
|
||||
name: 'task2',
|
||||
},
|
||||
{
|
||||
name: 'task3',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
jest.mock('../../utils/EntityLineageUtils', () => ({
|
||||
dragHandle: jest.fn(),
|
||||
getDataLabel: jest
|
||||
.fn()
|
||||
.mockReturnValue(<span data-testid="lineage-entity">datalabel</span>),
|
||||
getDeletedLineagePlaceholder: jest
|
||||
.fn()
|
||||
.mockReturnValue(<p>Task data is not available for deleted entities.</p>),
|
||||
getHeaderLabel: jest.fn().mockReturnValue(<p>Header label</p>),
|
||||
getLayoutedElements: jest.fn().mockReturnValue([]),
|
||||
getLineageData: jest.fn().mockReturnValue([]),
|
||||
getModalBodyText: jest.fn(),
|
||||
onLoad: jest.fn(),
|
||||
onNodeContextMenu: jest.fn(),
|
||||
onNodeMouseEnter: jest.fn(),
|
||||
onNodeMouseLeave: jest.fn(),
|
||||
onNodeMouseMove: jest.fn(),
|
||||
getUniqueFlowElements: jest.fn().mockReturnValue([]),
|
||||
}));
|
||||
|
||||
describe('Test PipelineDetails component', () => {
|
||||
it('Checks if the PipelineDetails component has all the proper components rendered', async () => {
|
||||
const { container } = render(<TasksDAGView {...TasksDAGViewProps} />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
const reactFlowElement = await findByTestId(
|
||||
container,
|
||||
'react-flow-component'
|
||||
);
|
||||
|
||||
expect(reactFlowElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import ReactFlow, {
|
||||
ArrowHeadType,
|
||||
Edge,
|
||||
Elements,
|
||||
Node,
|
||||
} from 'react-flow-renderer';
|
||||
import { Task } from '../../generated/entity/data/pipeline';
|
||||
import { EntityReference } from '../../generated/type/entityReference';
|
||||
import { getEntityName, replaceSpaceWith_ } from '../../utils/CommonUtils';
|
||||
import { getLayoutedElements, onLoad } from '../../utils/EntityLineageUtils';
|
||||
|
||||
export interface Props {
|
||||
tasks: Task[];
|
||||
}
|
||||
|
||||
const TasksDAGView = ({ tasks }: Props) => {
|
||||
const [elements, setElements] = useState<Elements>([]);
|
||||
|
||||
const getNodeType = useCallback(
|
||||
(index: number) => {
|
||||
return index === 0
|
||||
? 'input'
|
||||
: index === tasks.length - 1
|
||||
? 'output'
|
||||
: 'default';
|
||||
},
|
||||
[tasks]
|
||||
);
|
||||
|
||||
const nodes: Node[] = useMemo(() => {
|
||||
const posY = 0;
|
||||
let posX = 0;
|
||||
const deltaX = 250;
|
||||
|
||||
return tasks.map((task, index) => {
|
||||
posX += deltaX;
|
||||
|
||||
return {
|
||||
className: 'leaf-node',
|
||||
id: replaceSpaceWith_(task.name),
|
||||
type: getNodeType(index),
|
||||
data: { label: getEntityName(task as EntityReference) },
|
||||
position: { x: posX, y: posY },
|
||||
};
|
||||
});
|
||||
}, [tasks]);
|
||||
|
||||
const edges: Edge[] = useMemo(() => {
|
||||
return tasks.reduce((prev, task) => {
|
||||
const src = replaceSpaceWith_(task.name);
|
||||
const taskEdges = (task.downstreamTasks || []).map((dwTask) => {
|
||||
const dest = replaceSpaceWith_(dwTask);
|
||||
|
||||
return {
|
||||
arrowHeadType: ArrowHeadType.ArrowClosed,
|
||||
id: `${src}-${dest}`,
|
||||
type: 'straight',
|
||||
source: src,
|
||||
target: dest,
|
||||
label: '',
|
||||
} as Edge;
|
||||
});
|
||||
|
||||
return [...prev, ...taskEdges];
|
||||
}, [] as Edge[]);
|
||||
}, [tasks]);
|
||||
|
||||
useEffect(() => {
|
||||
setElements(getLayoutedElements([...nodes, ...edges]));
|
||||
}, [nodes, edges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
data-testid="react-flow-component"
|
||||
elements={elements}
|
||||
maxZoom={2}
|
||||
minZoom={0.5}
|
||||
selectNodesOnDrag={false}
|
||||
zoomOnDoubleClick={false}
|
||||
zoomOnScroll={false}
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TasksDAGView;
|
@ -627,3 +627,7 @@ export const getExploreLinkByFilter = (
|
||||
`${filter}=${getOwnerIds(filter, userDetails, nonSecureUserDetails).join()}`
|
||||
);
|
||||
};
|
||||
|
||||
export const replaceSpaceWith_ = (text: string) => {
|
||||
return text.replace(/\s/g, '_');
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user