mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-04 11:33:07 +00:00
Add Design: Entity detail drawer for lineage tab. (#927)
* Add Design: Entity detail drawer for lineage tab. * integrated apis for pipeline and dashboard to show data on drawer. * fixed mainentity should not be clickable. * added support for outside click * addressing review comments * minor tweaks
This commit is contained in:
parent
f15c430023
commit
451b96e15e
@ -0,0 +1,245 @@
|
|||||||
|
import { AxiosError, AxiosResponse } from 'axios';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { getDatabase } from '../../axiosAPIs/databaseAPI';
|
||||||
|
import { getPipelineByFqn } from '../../axiosAPIs/pipelineAPI';
|
||||||
|
import { getServiceById } from '../../axiosAPIs/serviceAPI';
|
||||||
|
import { getTableDetailsByFQN } from '../../axiosAPIs/tableAPI';
|
||||||
|
import { EntityType } from '../../enums/entity.enum';
|
||||||
|
import { Dashboard } from '../../generated/entity/data/dashboard';
|
||||||
|
import { Pipeline } from '../../generated/entity/data/pipeline';
|
||||||
|
import { Table } from '../../generated/entity/data/table';
|
||||||
|
import { Topic } from '../../generated/entity/data/topic';
|
||||||
|
import useToastContext from '../../hooks/useToastContext';
|
||||||
|
import { getEntityOverview, getEntityTags } from '../../utils/EntityUtils';
|
||||||
|
import { getEntityIcon, getEntityLink } from '../../utils/TableUtils';
|
||||||
|
import { SelectedNode } from '../EntityLineage/EntityLineage.interface';
|
||||||
|
import Loader from '../Loader/Loader';
|
||||||
|
import Tags from '../tags/tags';
|
||||||
|
import { LineageDrawerProps } from './EntityInfoDrawer.interface';
|
||||||
|
import './EntityInfoDrawer.style.css';
|
||||||
|
|
||||||
|
const getHeaderLabel = (
|
||||||
|
v = '',
|
||||||
|
type: string,
|
||||||
|
isMainNode: boolean,
|
||||||
|
separator = '.'
|
||||||
|
) => {
|
||||||
|
const length = v.split(separator).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isMainNode ? (
|
||||||
|
<span
|
||||||
|
className="tw-break-words description-text tw-self-center tw-font-normal"
|
||||||
|
data-testid="lineage-entity">
|
||||||
|
{v.split(separator)[length - 1]}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className="tw-break-words description-text tw-self-center link-text tw-font-normal"
|
||||||
|
data-testid="lineage-entity">
|
||||||
|
<Link to={getEntityLink(type, v)}>
|
||||||
|
{v.split(separator)[length - 1]}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntityInfoDrawer = ({
|
||||||
|
show,
|
||||||
|
onCancel,
|
||||||
|
selectedNode,
|
||||||
|
isMainNode = false,
|
||||||
|
}: LineageDrawerProps) => {
|
||||||
|
const showToast = useToastContext();
|
||||||
|
const [entityDetail, setEntityDetail] = useState<
|
||||||
|
Partial<Table> & Partial<Pipeline> & Partial<Dashboard> & Partial<Topic>
|
||||||
|
>(
|
||||||
|
{} as Partial<Table> &
|
||||||
|
Partial<Pipeline> &
|
||||||
|
Partial<Dashboard> &
|
||||||
|
Partial<Topic>
|
||||||
|
);
|
||||||
|
const [serviceType, setServiceType] = useState<string>('');
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const fetchEntityDetail = (selectedNode: SelectedNode) => {
|
||||||
|
switch (selectedNode.type) {
|
||||||
|
case EntityType.TABLE: {
|
||||||
|
setIsLoading(true);
|
||||||
|
getTableDetailsByFQN(selectedNode.name, [
|
||||||
|
'tags',
|
||||||
|
'owner',
|
||||||
|
'columns',
|
||||||
|
'usageSummary',
|
||||||
|
'tableProfile',
|
||||||
|
'database',
|
||||||
|
])
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
getDatabase(res.data.database.id, 'service')
|
||||||
|
.then((resDB: AxiosResponse) => {
|
||||||
|
getServiceById('databaseServices', resDB.data.service?.id).then(
|
||||||
|
(resService: AxiosResponse) => {
|
||||||
|
setServiceType(resService.data.serviceType);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
const msg = err.message;
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
body:
|
||||||
|
msg ?? `Error while getting ${selectedNode.name} details`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setEntityDetail(res.data);
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
const msg = err.message;
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
body: msg ?? `Error while getting ${selectedNode.name} details`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EntityType.PIPELINE: {
|
||||||
|
setIsLoading(true);
|
||||||
|
getPipelineByFqn(selectedNode.name, ['tags', 'owner', 'service'])
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
getServiceById('pipelineServices', res.data.service?.id)
|
||||||
|
.then((serviceRes: AxiosResponse) => {
|
||||||
|
setServiceType(serviceRes.data.serviceType);
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
const msg = err.message;
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
body:
|
||||||
|
msg ?? `Error while getting ${selectedNode.name} service`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setEntityDetail(res.data);
|
||||||
|
setIsLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err: AxiosError) => {
|
||||||
|
const msg = err.message;
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
body: msg ?? `Error while getting ${selectedNode.name} details`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchEntityDetail(selectedNode);
|
||||||
|
}, [selectedNode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('side-drawer', { open: show })}>
|
||||||
|
<header className="tw-flex tw-justify-between">
|
||||||
|
<p className="tw-flex">
|
||||||
|
<span className="tw-mr-2">{getEntityIcon(selectedNode.type)}</span>
|
||||||
|
{getHeaderLabel(selectedNode.name, selectedNode.type, isMainNode)}
|
||||||
|
</p>
|
||||||
|
<div className="tw-flex">
|
||||||
|
<svg
|
||||||
|
className="tw-w-5 tw-h-5 tw-ml-1 tw-cursor-pointer"
|
||||||
|
data-testid="closeDrawer"
|
||||||
|
fill="none"
|
||||||
|
stroke="#6B7280"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
onClick={() => onCancel(false)}>
|
||||||
|
<path
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<hr className="tw-mt-3 tw-border-primary-hover-lite" />
|
||||||
|
{isLoading ? (
|
||||||
|
<Loader />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<section className="tw-mt-1">
|
||||||
|
<span className="tw-text-grey-muted">Overview</span>
|
||||||
|
<div className="tw-flex tw-flex-col">
|
||||||
|
{getEntityOverview(
|
||||||
|
selectedNode.type,
|
||||||
|
entityDetail,
|
||||||
|
serviceType
|
||||||
|
).map((d) => {
|
||||||
|
return (
|
||||||
|
<p className="tw-py-1.5" key={d.name}>
|
||||||
|
{d.name && <span>{d.name}:</span>}
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
{ 'tw-ml-2': d.name },
|
||||||
|
{
|
||||||
|
'link-text': d.isLink,
|
||||||
|
}
|
||||||
|
)}>
|
||||||
|
{d.isLink ? (
|
||||||
|
<Link
|
||||||
|
target={d.isExternal ? '_blank' : '_self'}
|
||||||
|
to={{ pathname: d.url }}>
|
||||||
|
{d.value}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
d.value
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<hr className="tw-mt-3 tw-border-primary-hover-lite" />
|
||||||
|
<section className="tw-mt-1">
|
||||||
|
<span className="tw-text-grey-muted">Tags</span>
|
||||||
|
<div className="tw-flex tw-flex-wrap tw-pt-1.5">
|
||||||
|
{getEntityTags(selectedNode.type, entityDetail).length > 0 ? (
|
||||||
|
getEntityTags(selectedNode.type, entityDetail).map((t) => {
|
||||||
|
return <Tags key={t} tag={`#${t}`} />;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<p className="tw-text-xs tw-text-grey-muted">No Tags added</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<hr className="tw-mt-3 tw-border-primary-hover-lite" />
|
||||||
|
<section className="tw-mt-1">
|
||||||
|
<span className="tw-text-grey-muted">Description</span>
|
||||||
|
<div>
|
||||||
|
{entityDetail.description ?? (
|
||||||
|
<p className="tw-text-xs tw-text-grey-muted">
|
||||||
|
No description added
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntityInfoDrawer;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { SelectedNode } from '../EntityLineage/EntityLineage.interface';
|
||||||
|
|
||||||
|
export interface LineageDrawerProps {
|
||||||
|
show: boolean;
|
||||||
|
onCancel: (value: boolean) => void;
|
||||||
|
selectedNode: SelectedNode;
|
||||||
|
isMainNode: boolean;
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
.side-drawer {
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 325px;
|
||||||
|
z-index: 200;
|
||||||
|
margin-right: -16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
transform: translateX(100%);
|
||||||
|
border-left: 1px solid #d9ceee;
|
||||||
|
transition: transform 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backdrop {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(48, 46, 54, 0.6);
|
||||||
|
z-index: 100;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-drawer.open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
@ -23,7 +23,9 @@ import {
|
|||||||
EntityLineage,
|
EntityLineage,
|
||||||
} from '../../generated/type/entityLineage';
|
} from '../../generated/type/entityLineage';
|
||||||
import { EntityReference } from '../../generated/type/entityReference';
|
import { EntityReference } from '../../generated/type/entityReference';
|
||||||
|
import { getEntityIcon } from '../../utils/TableUtils';
|
||||||
|
import EntityInfoDrawer from '../EntityInfoDrawer/EntityInfoDrawer.component';
|
||||||
|
import { SelectedNode } from './EntityLineage.interface';
|
||||||
const onLoad = (reactFlowInstance: OnLoadParams) => {
|
const onLoad = (reactFlowInstance: OnLoadParams) => {
|
||||||
reactFlowInstance.fitView();
|
reactFlowInstance.fitView();
|
||||||
reactFlowInstance.zoomTo(1);
|
reactFlowInstance.zoomTo(1);
|
||||||
@ -49,16 +51,21 @@ const getDataLabel = (v = '', separator = '.') => {
|
|||||||
const length = v.split(separator).length;
|
const length = v.split(separator).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="tw-break-words description-text" data-testid="lineage-entity">
|
<span
|
||||||
|
className="tw-break-words description-text tw-self-center"
|
||||||
|
data-testid="lineage-entity">
|
||||||
{v.split(separator)[length - 1]}
|
{v.split(separator)[length - 1]}
|
||||||
</p>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const positionX = 150;
|
const positionX = 150;
|
||||||
const positionY = 60;
|
const positionY = 60;
|
||||||
|
|
||||||
const getLineageData = (entityLineage: EntityLineage) => {
|
const getLineageData = (
|
||||||
|
entityLineage: EntityLineage,
|
||||||
|
onSelect: (state: boolean, value: SelectedNode) => void
|
||||||
|
) => {
|
||||||
const [x, y] = [0, 0];
|
const [x, y] = [0, 0];
|
||||||
const nodes = entityLineage['nodes'];
|
const nodes = entityLineage['nodes'];
|
||||||
let upstreamEdges: Array<LineageEdge & { isMapped: boolean }> =
|
let upstreamEdges: Array<LineageEdge & { isMapped: boolean }> =
|
||||||
@ -96,7 +103,22 @@ const getLineageData = (entityLineage: EntityLineage) => {
|
|||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
type: 'default',
|
type: 'default',
|
||||||
className: 'leaf-node',
|
className: 'leaf-node',
|
||||||
data: { label: getDataLabel(node.name as string) },
|
data: {
|
||||||
|
label: (
|
||||||
|
<p
|
||||||
|
className="tw-flex"
|
||||||
|
onClick={() =>
|
||||||
|
onSelect(true, {
|
||||||
|
name: node.name as string,
|
||||||
|
type: node.type,
|
||||||
|
id: `node-${node.id}-${depth}`,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<span className="tw-mr-2">{getEntityIcon(node.type)}</span>
|
||||||
|
{getDataLabel(node.name as string)}
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
position: {
|
position: {
|
||||||
x: -positionX * 2 * depth,
|
x: -positionX * 2 * depth,
|
||||||
y: y + positionY * upDepth,
|
y: y + positionY * upDepth,
|
||||||
@ -141,7 +163,22 @@ const getLineageData = (entityLineage: EntityLineage) => {
|
|||||||
targetPosition: Position.Left,
|
targetPosition: Position.Left,
|
||||||
type: 'default',
|
type: 'default',
|
||||||
className: 'leaf-node',
|
className: 'leaf-node',
|
||||||
data: { label: getDataLabel(node.name as string) },
|
data: {
|
||||||
|
label: (
|
||||||
|
<span
|
||||||
|
className="tw-flex"
|
||||||
|
onClick={() =>
|
||||||
|
onSelect(true, {
|
||||||
|
name: node.name as string,
|
||||||
|
type: node.type,
|
||||||
|
id: `node-${node.id}-${depth}`,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<span className="tw-mr-2">{getEntityIcon(node.type)}</span>
|
||||||
|
{getDataLabel(node.name as string)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
position: {
|
position: {
|
||||||
x: positionX * 2 * depth,
|
x: positionX * 2 * depth,
|
||||||
y: y + positionY * downDepth,
|
y: y + positionY * downDepth,
|
||||||
@ -229,7 +266,21 @@ const getLineageData = (entityLineage: EntityLineage) => {
|
|||||||
: 'output'
|
: 'output'
|
||||||
: 'input',
|
: 'input',
|
||||||
className: 'leaf-node core',
|
className: 'leaf-node core',
|
||||||
data: { label: getDataLabel(mainNode.name as string) },
|
data: {
|
||||||
|
label: (
|
||||||
|
<p
|
||||||
|
className="tw-flex"
|
||||||
|
onClick={() =>
|
||||||
|
onSelect(true, {
|
||||||
|
name: mainNode.name as string,
|
||||||
|
type: mainNode.type,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<span className="tw-mr-2">{getEntityIcon(mainNode.type)}</span>
|
||||||
|
{getDataLabel(mainNode.name as string)}
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
},
|
||||||
position: { x: x, y: y },
|
position: { x: x, y: y },
|
||||||
},
|
},
|
||||||
...UPStreamNodes.map((up) => {
|
...UPStreamNodes.map((up) => {
|
||||||
@ -257,45 +308,90 @@ const Entitylineage: FunctionComponent<{ entityLineage: EntityLineage }> = ({
|
|||||||
}: {
|
}: {
|
||||||
entityLineage: EntityLineage;
|
entityLineage: EntityLineage;
|
||||||
}) => {
|
}) => {
|
||||||
const [elements, setElements] = useState<Elements>(
|
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
|
||||||
getLineageData(entityLineage) as Elements
|
const [selectedNode, setSelectedNode] = useState<SelectedNode>(
|
||||||
|
{} as SelectedNode
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectNodeHandler = (state: boolean, value: SelectedNode) => {
|
||||||
|
setIsDrawerOpen(state);
|
||||||
|
setSelectedNode(value);
|
||||||
|
};
|
||||||
|
const [elements, setElements] = useState<Elements>(
|
||||||
|
getLineageData(entityLineage, selectNodeHandler) as Elements
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeDrawer = (value: boolean) => {
|
||||||
|
setIsDrawerOpen(value);
|
||||||
|
|
||||||
|
setElements((prevElements) => {
|
||||||
|
return prevElements.map((el) => {
|
||||||
|
if (el.id === selectedNode.id) {
|
||||||
|
return { ...el, className: 'leaf-node' };
|
||||||
|
} else {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onElementsRemove = (elementsToRemove: Elements) =>
|
const onElementsRemove = (elementsToRemove: Elements) =>
|
||||||
setElements((els) => removeElements(elementsToRemove, els));
|
setElements((els) => removeElements(elementsToRemove, els));
|
||||||
const onConnect = (params: Edge | Connection) =>
|
const onConnect = (params: Edge | Connection) =>
|
||||||
setElements((els) => addEdge(params, els));
|
setElements((els) => addEdge(params, els));
|
||||||
|
|
||||||
|
const onElementClick = (el: FlowElement) => {
|
||||||
|
setElements((prevElements) => {
|
||||||
|
return prevElements.map((preEl) => {
|
||||||
|
if (preEl.id === el.id) {
|
||||||
|
return { ...preEl, className: `${preEl.className} selected-node` };
|
||||||
|
} else {
|
||||||
|
return { ...preEl, className: 'leaf-node' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setElements(getLineageData(entityLineage) as Elements);
|
setElements(getLineageData(entityLineage, selectNodeHandler) as Elements);
|
||||||
}, [entityLineage]);
|
}, [entityLineage]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tw-w-full tw-h-full">
|
<div className="tw-relative tw-h-full tw--ml-4">
|
||||||
{(entityLineage?.downstreamEdges ?? []).length > 0 ||
|
<div className="tw-w-full tw-h-full">
|
||||||
(entityLineage.upstreamEdges ?? []).length ? (
|
{(entityLineage?.downstreamEdges ?? []).length > 0 ||
|
||||||
<ReactFlowProvider>
|
(entityLineage.upstreamEdges ?? []).length ? (
|
||||||
<ReactFlow
|
<ReactFlowProvider>
|
||||||
panOnScroll
|
<ReactFlow
|
||||||
elements={elements as Elements}
|
panOnScroll
|
||||||
nodesConnectable={false}
|
elements={elements as Elements}
|
||||||
onConnect={onConnect}
|
nodesConnectable={false}
|
||||||
onElementsRemove={onElementsRemove}
|
onConnect={onConnect}
|
||||||
onLoad={onLoad}
|
onElementClick={(_e, el) => onElementClick(el)}
|
||||||
onNodeContextMenu={onNodeContextMenu}
|
onElementsRemove={onElementsRemove}
|
||||||
onNodeMouseEnter={onNodeMouseEnter}
|
onLoad={onLoad}
|
||||||
onNodeMouseLeave={onNodeMouseLeave}
|
onNodeContextMenu={onNodeContextMenu}
|
||||||
onNodeMouseMove={onNodeMouseMove}>
|
onNodeMouseEnter={onNodeMouseEnter}
|
||||||
<Controls
|
onNodeMouseLeave={onNodeMouseLeave}
|
||||||
className="tw-top-1 tw-left-1 tw-bottom-full"
|
onNodeMouseMove={onNodeMouseMove}>
|
||||||
showInteractive={false}
|
<Controls
|
||||||
/>
|
className="tw-top-1 tw-left-1 tw-bottom-full tw-ml-4 tw-mt-4"
|
||||||
</ReactFlow>
|
showInteractive={false}
|
||||||
</ReactFlowProvider>
|
/>
|
||||||
) : (
|
</ReactFlow>
|
||||||
<div className="tw-flex tw-justify-center tw-font-medium tw-items-center tw-border tw-border-main tw-rounded-md tw-p-8">
|
</ReactFlowProvider>
|
||||||
No Lineage data available
|
) : (
|
||||||
</div>
|
<div className="tw-flex tw-justify-center tw-font-medium tw-items-center tw-border tw-border-main tw-rounded-md tw-p-8">
|
||||||
)}
|
No Lineage data available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<EntityInfoDrawer
|
||||||
|
isMainNode={selectedNode.name === entityLineage.entity.name}
|
||||||
|
selectedNode={selectedNode}
|
||||||
|
show={isDrawerOpen}
|
||||||
|
onCancel={closeDrawer}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
export interface SelectedNode {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
@ -308,6 +308,11 @@
|
|||||||
}
|
}
|
||||||
.leaf-node.selected,
|
.leaf-node.selected,
|
||||||
.leaf-node.selected:hover {
|
.leaf-node.selected:hover {
|
||||||
|
@apply tw-border-main;
|
||||||
|
box-shadow: 0 0 0 0.5px #e2dce4;
|
||||||
|
}
|
||||||
|
.leaf-node.selected-node,
|
||||||
|
.leaf-node.selected-node:hover {
|
||||||
@apply tw-border-primary-active;
|
@apply tw-border-primary-active;
|
||||||
box-shadow: 0 0 0 0.5px #7147e8;
|
box-shadow: 0 0 0 0.5px #7147e8;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,206 @@
|
|||||||
|
import { isNil } from 'lodash';
|
||||||
|
import {
|
||||||
|
getDatabaseDetailsPath,
|
||||||
|
getServiceDetailsPath,
|
||||||
|
getTeamDetailsPath,
|
||||||
|
} from '../constants/constants';
|
||||||
|
import { EntityType } from '../enums/entity.enum';
|
||||||
|
import { Dashboard } from '../generated/entity/data/dashboard';
|
||||||
|
import { Pipeline } from '../generated/entity/data/pipeline';
|
||||||
|
import { Table } from '../generated/entity/data/table';
|
||||||
|
import { Topic } from '../generated/entity/data/topic';
|
||||||
|
import { TagLabel } from '../generated/type/tagLabel';
|
||||||
|
import { getPartialNameFromFQN } from './CommonUtils';
|
||||||
|
import {
|
||||||
|
getOwnerFromId,
|
||||||
|
getTierFromTableTags,
|
||||||
|
getUsagePercentile,
|
||||||
|
} from './TableUtils';
|
||||||
|
import { getTableTags } from './TagsUtils';
|
||||||
|
import { getRelativeDay } from './TimeUtils';
|
||||||
|
|
||||||
|
export const getEntityTags = (
|
||||||
|
type: string,
|
||||||
|
entityDetail: Partial<Table> &
|
||||||
|
Partial<Pipeline> &
|
||||||
|
Partial<Dashboard> &
|
||||||
|
Partial<Topic>
|
||||||
|
): Array<string | undefined> => {
|
||||||
|
switch (type) {
|
||||||
|
case EntityType.TABLE: {
|
||||||
|
const tableTags: Array<TagLabel> = [
|
||||||
|
...getTableTags(entityDetail.columns || []),
|
||||||
|
...(entityDetail.tags || []),
|
||||||
|
];
|
||||||
|
|
||||||
|
return tableTags.map((t) => t.tagFQN);
|
||||||
|
}
|
||||||
|
case EntityType.PIPELINE: {
|
||||||
|
return entityDetail.tags?.map((t) => t.tagFQN) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEntityOverview = (
|
||||||
|
type: string,
|
||||||
|
entityDetail: Partial<Table> &
|
||||||
|
Partial<Pipeline> &
|
||||||
|
Partial<Dashboard> &
|
||||||
|
Partial<Topic>,
|
||||||
|
serviceType: string
|
||||||
|
): Array<{
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
isLink: boolean;
|
||||||
|
isExternal?: boolean;
|
||||||
|
url?: string;
|
||||||
|
}> => {
|
||||||
|
switch (type) {
|
||||||
|
case EntityType.TABLE: {
|
||||||
|
const { fullyQualifiedName, owner, tags, usageSummary, tableProfile } =
|
||||||
|
entityDetail;
|
||||||
|
const [service, database] = getPartialNameFromFQN(
|
||||||
|
fullyQualifiedName ?? '',
|
||||||
|
['service', 'database'],
|
||||||
|
'.'
|
||||||
|
).split('.');
|
||||||
|
const ownerValue = getOwnerFromId(owner?.id);
|
||||||
|
const tier = getTierFromTableTags(tags || []);
|
||||||
|
const usage = !isNil(usageSummary?.weeklyStats?.percentileRank)
|
||||||
|
? getUsagePercentile(usageSummary?.weeklyStats?.percentileRank || 0)
|
||||||
|
: '--';
|
||||||
|
const queries = usageSummary?.weeklyStats?.count.toLocaleString() || '--';
|
||||||
|
const getProfilerRowDiff = (tableProfile: Table['tableProfile']) => {
|
||||||
|
let retDiff;
|
||||||
|
if (tableProfile && tableProfile.length > 0) {
|
||||||
|
let rowDiff: string | number = tableProfile[0].rowCount || 0;
|
||||||
|
const dayDiff = getRelativeDay(
|
||||||
|
tableProfile[0].profileDate
|
||||||
|
? new Date(tableProfile[0].profileDate).getTime()
|
||||||
|
: Date.now()
|
||||||
|
);
|
||||||
|
if (tableProfile.length > 1) {
|
||||||
|
rowDiff = rowDiff - (tableProfile[1].rowCount || 0);
|
||||||
|
}
|
||||||
|
retDiff = `${(rowDiff >= 0 ? '+' : '') + rowDiff} rows ${dayDiff}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return retDiff;
|
||||||
|
};
|
||||||
|
|
||||||
|
const profilerRowDiff = getProfilerRowDiff(tableProfile);
|
||||||
|
const overview = [
|
||||||
|
{
|
||||||
|
name: 'Service',
|
||||||
|
value: service,
|
||||||
|
url: getServiceDetailsPath(service, serviceType),
|
||||||
|
isLink: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Database',
|
||||||
|
value: database,
|
||||||
|
url: getDatabaseDetailsPath(
|
||||||
|
getPartialNameFromFQN(
|
||||||
|
fullyQualifiedName ?? '',
|
||||||
|
['service', 'database'],
|
||||||
|
'.'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
isLink: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Owner',
|
||||||
|
value: ownerValue?.displayName || ownerValue?.name || '--',
|
||||||
|
url: getTeamDetailsPath(owner?.name || ''),
|
||||||
|
isLink: ownerValue
|
||||||
|
? ownerValue.type === 'team'
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tier',
|
||||||
|
value: tier ? tier.split('.')[1] : '--',
|
||||||
|
isLink: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Usage',
|
||||||
|
value: usage,
|
||||||
|
isLink: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Queries',
|
||||||
|
value: `${queries} past week`,
|
||||||
|
isLink: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rows',
|
||||||
|
value:
|
||||||
|
tableProfile && tableProfile[0]?.rowCount
|
||||||
|
? tableProfile[0].rowCount
|
||||||
|
: '--',
|
||||||
|
isLink: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Columns',
|
||||||
|
value:
|
||||||
|
tableProfile && tableProfile[0]?.columnCount
|
||||||
|
? tableProfile[0].columnCount
|
||||||
|
: '--',
|
||||||
|
isLink: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (!isNil(profilerRowDiff)) {
|
||||||
|
overview.push({ value: profilerRowDiff, name: '', isLink: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return overview;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EntityType.PIPELINE: {
|
||||||
|
const { owner, tags, pipelineUrl, service, fullyQualifiedName } =
|
||||||
|
entityDetail;
|
||||||
|
const ownerValue = getOwnerFromId(owner?.id);
|
||||||
|
const tier = getTierFromTableTags(tags || []);
|
||||||
|
|
||||||
|
const overview = [
|
||||||
|
{
|
||||||
|
name: 'Service',
|
||||||
|
value: service?.name as string,
|
||||||
|
url: getServiceDetailsPath(service?.name as string, serviceType),
|
||||||
|
isLink: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Owner',
|
||||||
|
value: ownerValue?.displayName || ownerValue?.name || '--',
|
||||||
|
url: getTeamDetailsPath(owner?.name || ''),
|
||||||
|
isLink: ownerValue
|
||||||
|
? ownerValue.type === 'team'
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tier',
|
||||||
|
value: tier ? tier.split('.')[1] : '--',
|
||||||
|
isLink: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${serviceType} url`,
|
||||||
|
value: fullyQualifiedName?.split('.')[1] as string,
|
||||||
|
url: pipelineUrl as string,
|
||||||
|
isLink: true,
|
||||||
|
isExternal: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return overview;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
getPipelineDetailsPath,
|
getPipelineDetailsPath,
|
||||||
getTopicDetailsPath,
|
getTopicDetailsPath,
|
||||||
} from '../constants/constants';
|
} from '../constants/constants';
|
||||||
|
import { EntityType } from '../enums/entity.enum';
|
||||||
import { SearchIndex } from '../enums/search.enum';
|
import { SearchIndex } from '../enums/search.enum';
|
||||||
import { ConstraintTypes } from '../enums/table.enum';
|
import { ConstraintTypes } from '../enums/table.enum';
|
||||||
import { Column, Table } from '../generated/entity/data/table';
|
import { Column, Table } from '../generated/entity/data/table';
|
||||||
@ -164,15 +165,19 @@ export const getEntityLink = (
|
|||||||
) => {
|
) => {
|
||||||
switch (indexType) {
|
switch (indexType) {
|
||||||
case SearchIndex.TOPIC:
|
case SearchIndex.TOPIC:
|
||||||
|
case EntityType.TOPIC:
|
||||||
return getTopicDetailsPath(fullyQualifiedName);
|
return getTopicDetailsPath(fullyQualifiedName);
|
||||||
|
|
||||||
case SearchIndex.DASHBOARD:
|
case SearchIndex.DASHBOARD:
|
||||||
|
case EntityType.DASHBOARD:
|
||||||
return getDashboardDetailsPath(fullyQualifiedName);
|
return getDashboardDetailsPath(fullyQualifiedName);
|
||||||
|
|
||||||
case SearchIndex.PIPELINE:
|
case SearchIndex.PIPELINE:
|
||||||
|
case EntityType.PIPELINE:
|
||||||
return getPipelineDetailsPath(fullyQualifiedName);
|
return getPipelineDetailsPath(fullyQualifiedName);
|
||||||
|
|
||||||
case SearchIndex.TABLE:
|
case SearchIndex.TABLE:
|
||||||
|
case EntityType.TABLE:
|
||||||
default:
|
default:
|
||||||
return getDatasetDetailsPath(fullyQualifiedName);
|
return getDatasetDetailsPath(fullyQualifiedName);
|
||||||
}
|
}
|
||||||
@ -182,20 +187,24 @@ export const getEntityIcon = (indexType: string) => {
|
|||||||
let icon = '';
|
let icon = '';
|
||||||
switch (indexType) {
|
switch (indexType) {
|
||||||
case SearchIndex.TOPIC:
|
case SearchIndex.TOPIC:
|
||||||
|
case EntityType.TOPIC:
|
||||||
icon = 'topic';
|
icon = 'topic';
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SearchIndex.DASHBOARD:
|
case SearchIndex.DASHBOARD:
|
||||||
|
case EntityType.DASHBOARD:
|
||||||
icon = 'dashboard';
|
icon = 'dashboard';
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case SearchIndex.PIPELINE:
|
case SearchIndex.PIPELINE:
|
||||||
|
case EntityType.PIPELINE:
|
||||||
icon = 'pipeline';
|
icon = 'pipeline';
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SearchIndex.TABLE:
|
case SearchIndex.TABLE:
|
||||||
|
case EntityType.TABLE:
|
||||||
default:
|
default:
|
||||||
icon = 'table';
|
icon = 'table';
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user