Adding Lineage node expansion to show columns . (#1393)

* Adding Lineage node expansion to show columns .

* Adding support for getting columns of expanded node.

* Refactoring.

* Reafctoring and style changes.

* Minor changes

* Changing prop type.

* minor style changes.
This commit is contained in:
Sachin Chaurasiya 2021-11-26 20:37:33 +05:30 committed by GitHub
parent 6582f38a37
commit c74d7612bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 337 additions and 145 deletions

View File

@ -0,0 +1,4 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25 50C38.8071 50 50 38.8071 50 25C50 11.1929 38.8071 0 25 0C11.1929 0 0 11.1929 0 25C0 38.8071 11.1929 50 25 50Z" fill="#e0d6ff"/>
<path d="M37.5 25H12.5" stroke="#7147E8" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@ -187,7 +187,7 @@ const EntityInfoDrawer = ({
serviceType serviceType
).map((d) => { ).map((d) => {
return ( return (
<p className="tw-py-1.5 tw-flex" key={d.name}> <div className="tw-py-1.5 tw-flex" key={d.name}>
{d.name && <span>{d.name}:</span>} {d.name && <span>{d.name}:</span>}
<span <span
className={classNames( className={classNames(
@ -206,7 +206,7 @@ const EntityInfoDrawer = ({
d.value d.value
)} )}
</span> </span>
</p> </div>
); );
})} })}
</div> </div>

View File

@ -0,0 +1,77 @@
import classNames from 'classnames';
import React, { Fragment } from 'react';
import { Handle } from 'react-flow-renderer';
const handleStyles = { borderRadius: '50%', position: 'absolute', top: 10 };
const getHandle = (nodeType, isConnectable) => {
if (nodeType === 'output') {
return (
<Handle
isConnectable={isConnectable}
position="left"
style={{ ...handleStyles, left: '-14px' }}
type="target"
/>
);
} else if (nodeType === 'input') {
return (
<Handle
isConnectable={isConnectable}
position="right"
style={{ ...handleStyles, right: '-14px' }}
type="source"
/>
);
} else {
return (
<Fragment>
<Handle
isConnectable={isConnectable}
position="left"
style={{ ...handleStyles, left: '-14px' }}
type="target"
/>
<Handle
isConnectable={isConnectable}
position="right"
style={{ ...handleStyles, right: '-14px' }}
type="source"
/>
</Fragment>
);
}
};
const CustomNode = (props) => {
/* eslint-disable-next-line */
const { data, type, isConnectable } = props;
/* eslint-disable-next-line */
const { label, columns } = data;
return (
<div className="tw-relative nowheel ">
{getHandle(type, isConnectable)}
{/* Node label could be simple text or reactNode */}
<div className={classNames('tw-px-2')}>{label}</div>
{columns?.length ? <hr className="tw-my-2 tw--mx-3" /> : null}
<section
className={classNames('tw--mx-3 tw-px-3', {
'tw-h-36 tw-overflow-y-auto': columns?.length,
})}
id="table-columns">
<div className="tw-flex tw-flex-col tw-gap-y-1">
{columns?.map((c) => (
<p
className="tw-p-1 tw-rounded tw-border tw-text-primary"
key={c.name}>
{c.name}
</p>
))}
</div>
</section>
</div>
);
};
export default CustomNode;

View File

@ -1,8 +1,11 @@
import { AxiosResponse } from 'axios';
import { isEmpty } from 'lodash';
import { LeafNodes, LineagePos, LoadingNodeState } from 'Models'; import { LeafNodes, LineagePos, LoadingNodeState } from 'Models';
import React, { import React, {
FunctionComponent, FunctionComponent,
MouseEvent as ReactMouseEvent, MouseEvent as ReactMouseEvent,
useEffect, useEffect,
useRef,
useState, useState,
} from 'react'; } from 'react';
import ReactFlow, { import ReactFlow, {
@ -20,15 +23,20 @@ import ReactFlow, {
removeElements, removeElements,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { getTableDetails } from '../../axiosAPIs/tableAPI';
import { Column } from '../../generated/entity/data/table';
import { import {
Edge as LineageEdge, Edge as LineageEdge,
EntityLineage, EntityLineage,
} from '../../generated/type/entityLineage'; } from '../../generated/type/entityLineage';
import { EntityReference } from '../../generated/type/entityReference'; import { EntityReference } from '../../generated/type/entityReference';
import useToastContext from '../../hooks/useToastContext';
import { isLeafNode } from '../../utils/EntityUtils'; import { isLeafNode } from '../../utils/EntityUtils';
import SVGIcons from '../../utils/SvgUtils';
import { getEntityIcon } from '../../utils/TableUtils'; import { getEntityIcon } from '../../utils/TableUtils';
import EntityInfoDrawer from '../EntityInfoDrawer/EntityInfoDrawer.component'; import EntityInfoDrawer from '../EntityInfoDrawer/EntityInfoDrawer.component';
import Loader from '../Loader/Loader'; import Loader from '../Loader/Loader';
import CustomNode from './CustomNode.component';
import { EntityLineageProp, SelectedNode } from './EntityLineage.interface'; import { EntityLineageProp, SelectedNode } from './EntityLineage.interface';
const onLoad = (reactFlowInstance: OnLoadParams) => { const onLoad = (reactFlowInstance: OnLoadParams) => {
reactFlowInstance.fitView(); reactFlowInstance.fitView();
@ -51,8 +59,15 @@ const onNodeContextMenu = (_event: ReactMouseEvent, _node: Node | Edge) => {
_event.preventDefault(); _event.preventDefault();
}; };
const getDataLabel = (v = '', separator = '.') => { const dragHandle = (event: ReactMouseEvent) => {
event.stopPropagation();
};
const getDataLabel = (v = '', separator = '.', isTextOnly = false) => {
const length = v.split(separator).length; const length = v.split(separator).length;
if (isTextOnly) {
return v.split(separator)[length - 1];
}
return ( return (
<span <span
@ -63,6 +78,26 @@ const getDataLabel = (v = '', separator = '.') => {
); );
}; };
const getNoLineageDataPlaceholder = () => {
return (
<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>
Lineage is currently supported for Airflow. To enable lineage collection
from Airflow, Please follow the documentation
</span>
<Link
className="tw-ml-1"
target="_blank"
to={{
pathname:
'https://docs.open-metadata.org/install/metadata-ingestion/airflow/configure-airflow-lineage',
}}>
here
</Link>
</div>
);
};
const positionX = 150; const positionX = 150;
const positionY = 60; const positionY = 60;
@ -71,7 +106,8 @@ const getLineageData = (
onSelect: (state: boolean, value: SelectedNode) => void, onSelect: (state: boolean, value: SelectedNode) => void,
loadNodeHandler: (node: EntityReference, pos: LineagePos) => void, loadNodeHandler: (node: EntityReference, pos: LineagePos) => void,
lineageLeafNodes: LeafNodes, lineageLeafNodes: LeafNodes,
isNodeLoading: LoadingNodeState isNodeLoading: LoadingNodeState,
getNodeLable: (node: EntityReference) => React.ReactNode
) => { ) => {
const [x, y] = [0, 0]; const [x, y] = [0, 0];
const nodes = entityLineage['nodes']; const nodes = entityLineage['nodes'];
@ -89,9 +125,33 @@ const getLineageData = (
const DOWNStreamNodes: Elements = []; const DOWNStreamNodes: Elements = [];
const lineageEdges: Elements = []; const lineageEdges: Elements = [];
const makeNode = (
node: EntityReference,
pos: LineagePos,
depth: number,
posDepth: number
) => {
const [xVal, yVal] = [positionX * 2 * depth, y + positionY * posDepth];
return {
id: `node-${node.id}-${depth}`,
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: 'default',
className: 'leaf-node',
data: {
label: getNodeLable(node),
},
position: {
x: pos === 'from' ? -xVal : xVal,
y: yVal,
},
};
};
const getNodes = ( const getNodes = (
id: string, id: string,
pos: 'from' | 'to', pos: LineagePos,
depth: number, depth: number,
NodesArr: Array<EntityReference & { lDepth: number }> = [] NodesArr: Array<EntityReference & { lDepth: number }> = []
): Array<EntityReference & { lDepth: number }> => { ): Array<EntityReference & { lDepth: number }> => {
@ -104,25 +164,7 @@ const getLineageData = (
const node = nodes?.find((nd) => nd.id === up.fromEntity); const node = nodes?.find((nd) => nd.id === up.fromEntity);
if (node) { if (node) {
UPNodes.push(node); UPNodes.push(node);
UPStreamNodes.push({ UPStreamNodes.push(makeNode(node, 'from', depth, upDepth));
id: `node-${node.id}-${depth}`,
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: 'default',
className: 'leaf-node',
data: {
label: (
<p className="tw-flex">
<span className="tw-mr-2">{getEntityIcon(node.type)}</span>
{getDataLabel(node.name as string)}
</p>
),
},
position: {
x: -positionX * 2 * depth,
y: y + positionY * upDepth,
},
});
lineageEdges.push({ lineageEdges.push({
id: `edge-${up.fromEntity}-${id}-${depth}`, id: `edge-${up.fromEntity}-${id}-${depth}`,
source: `node-${node.id}-${depth}`, source: `node-${node.id}-${depth}`,
@ -156,25 +198,7 @@ const getLineageData = (
const node = nodes?.find((nd) => nd.id === down.toEntity); const node = nodes?.find((nd) => nd.id === down.toEntity);
if (node) { if (node) {
DOWNNodes.push(node); DOWNNodes.push(node);
DOWNStreamNodes.push({ DOWNStreamNodes.push(makeNode(node, 'to', depth, downDepth));
id: `node-${node.id}-${depth}`,
sourcePosition: Position.Right,
targetPosition: Position.Left,
type: 'default',
className: 'leaf-node',
data: {
label: (
<p className="tw-flex">
<span className="tw-mr-2">{getEntityIcon(node.type)}</span>
{getDataLabel(node.name as string)}
</p>
),
},
position: {
x: positionX * 2 * depth,
y: y + positionY * downDepth,
},
});
lineageEdges.push({ lineageEdges.push({
id: `edge-${id}-${down.toEntity}`, id: `edge-${id}-${down.toEntity}`,
source: edg ? edg.id : `node-${id}-${depth}`, source: edg ? edg.id : `node-${id}-${depth}`,
@ -258,19 +282,7 @@ const getLineageData = (
: 'input', : 'input',
className: 'leaf-node core', className: 'leaf-node core',
data: { data: {
label: ( label: getNodeLable(mainNode),
<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 },
}, },
@ -287,25 +299,28 @@ const getLineageData = (
data: { data: {
label: ( label: (
<div className="tw-flex"> <div className="tw-flex">
{!isLeafNode(lineageLeafNodes, node?.id as string, 'from') && <div
!up.id.includes(isNodeLoading.id as string) ? ( className="tw-pr-2 tw-self-center tw-cursor-pointer "
<p onClick={(e) => {
className="tw-mr-2 tw-self-center fas fa-chevron-left tw-cursor-pointer tw-text-primary" e.stopPropagation();
onClick={(e) => { onSelect(false, {} as SelectedNode);
e.stopPropagation(); if (node) {
onSelect(false, {} as SelectedNode); loadNodeHandler(node, 'from');
if (node) { }
loadNodeHandler(node, 'from'); }}>
} {!isLeafNode(
}} lineageLeafNodes,
/> node?.id as string,
) : null} 'from'
{isNodeLoading.state && ) && !up.id.includes(isNodeLoading.id as string) ? (
up.id.includes(isNodeLoading.id as string) ? ( <i className="fas fa-chevron-left tw-text-primary tw-mr-2" />
<div className="tw-mr-2 tw-self-center"> ) : null}
{isNodeLoading.state &&
up.id.includes(isNodeLoading.id as string) ? (
<Loader size="small" type="default" /> <Loader size="small" type="default" />
</div> ) : null}
) : null} </div>
<div>{up?.data?.label}</div> <div>{up?.data?.label}</div>
</div> </div>
), ),
@ -327,25 +342,24 @@ const getLineageData = (
<div className="tw-flex tw-justify-between"> <div className="tw-flex tw-justify-between">
<div>{down?.data?.label}</div> <div>{down?.data?.label}</div>
{!isLeafNode(lineageLeafNodes, node?.id as string, 'to') && <div
!down.id.includes(isNodeLoading.id as string) ? ( className="tw-pl-2 tw-self-center tw-cursor-pointer "
<p onClick={(e) => {
className="tw-ml-2 tw-self-center fas fa-chevron-right tw-cursor-pointer tw-text-primary" e.stopPropagation();
onClick={(e) => { onSelect(false, {} as SelectedNode);
e.stopPropagation(); if (node) {
onSelect(false, {} as SelectedNode); loadNodeHandler(node, 'to');
if (node) { }
loadNodeHandler(node, 'to'); }}>
} {!isLeafNode(lineageLeafNodes, node?.id as string, 'to') &&
}} !down.id.includes(isNodeLoading.id as string) ? (
/> <i className="fas fa-chevron-right tw-text-primary tw-ml-2" />
) : null} ) : null}
{isNodeLoading.state && {isNodeLoading.state &&
down.id.includes(isNodeLoading.id as string) ? ( down.id.includes(isNodeLoading.id as string) ? (
<div className="tw-ml-2 tw-self-center">
<Loader size="small" type="default" /> <Loader size="small" type="default" />
</div> ) : null}
) : null} </div>
</div> </div>
), ),
}, },
@ -363,25 +377,63 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
lineageLeafNodes, lineageLeafNodes,
isNodeLoading, isNodeLoading,
}: EntityLineageProp) => { }: EntityLineageProp) => {
const showToast = useToastContext();
const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false); const [isDrawerOpen, setIsDrawerOpen] = useState<boolean>(false);
const [selectedNode, setSelectedNode] = useState<SelectedNode>( const [selectedNode, setSelectedNode] = useState<SelectedNode>(
{} as SelectedNode {} as SelectedNode
); );
const expandButton = useRef<HTMLButtonElement | null>(null);
const [expandNode, setExpandNode] = useState<EntityReference | undefined>(
undefined
);
const [tableColumns, setTableColumns] = useState<Column[]>([] as Column[]);
const selectNodeHandler = (state: boolean, value: SelectedNode) => { const selectNodeHandler = (state: boolean, value: SelectedNode) => {
setIsDrawerOpen(state); setIsDrawerOpen(state);
setSelectedNode(value); setSelectedNode(value);
}; };
const [elements, setElements] = useState<Elements>(
getLineageData( const getNodeLable = (node: EntityReference) => {
return (
<>
{node.type === 'table' ? (
<button
className="tw-absolute tw--top-4 tw--left-5 tw-cursor-pointer tw-z-9999"
onClick={(e) => {
expandButton.current = expandButton.current
? null
: e.currentTarget;
setExpandNode(expandNode ? undefined : node);
setIsDrawerOpen(false);
}}>
<SVGIcons
alt="plus"
icon={expandNode?.id === node.id ? 'icon-minus' : 'icon-plus'}
width="16px"
/>
</button>
) : null}
<p className="tw-flex">
<span className="tw-mr-2">{getEntityIcon(node.type)}</span>
{getDataLabel(node.name as string)}
</p>
</>
);
};
const setElementsHandle = () => {
return getLineageData(
entityLineage, entityLineage,
selectNodeHandler, selectNodeHandler,
loadNodeHandler, loadNodeHandler,
lineageLeafNodes, lineageLeafNodes,
isNodeLoading isNodeLoading,
) as Elements getNodeLable
); ) as Elements;
};
const [elements, setElements] = useState<Elements>(setElementsHandle());
const closeDrawer = (value: boolean) => { const closeDrawer = (value: boolean) => {
setIsDrawerOpen(value); setIsDrawerOpen(value);
@ -395,66 +447,119 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
}); });
}); });
}; };
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) => { const onElementClick = (el: FlowElement) => {
const node = entityLineage.nodes?.find((n) => el.id.includes(n.id)); const node = [
selectNodeHandler(true, { ...(entityLineage.nodes as Array<EntityReference>),
name: node?.name as string, entityLineage.entity,
id: el.id, ].find((n) => el.id.includes(n.id));
type: node?.type as string, if (!expandButton.current) {
}); selectNodeHandler(true, {
setElements((prevElements) => { name: node?.name as string,
return prevElements.map((preEl) => { id: el.id,
if (preEl.id === el.id) { type: node?.type as string,
return { ...preEl, className: `${preEl.className} selected-node` }; });
setElements((prevElements) => {
return prevElements.map((preEl) => {
if (preEl.id === el.id) {
return { ...preEl, className: `${preEl.className} selected-node` };
} else {
return { ...preEl, className: 'leaf-node' };
}
});
});
} else {
expandButton.current = null;
}
};
const onNodeExpand = (tableColumns?: Column[]) => {
const elements = setElementsHandle();
setElements(
elements.map((preEl) => {
if (preEl.id.includes(expandNode?.id as string)) {
return {
...preEl,
className: `${preEl.className} selected-node`,
data: { ...preEl.data, columns: tableColumns },
};
} else { } else {
return { ...preEl, className: 'leaf-node' }; return { ...preEl, className: 'leaf-node' };
} }
}); })
}); );
};
const getTableColumns = (expandNode?: EntityReference) => {
if (expandNode) {
getTableDetails(expandNode.id, ['columns'])
.then((res: AxiosResponse) => {
const { columns } = res.data;
setTableColumns(columns);
})
.catch(() => {
showToast({
variant: 'error',
body: `Error while fetching ${getDataLabel(
expandNode.name,
'.',
true
)} columns`,
});
});
}
}; };
useEffect(() => { useEffect(() => {
setElements( setElements(setElementsHandle());
getLineageData( setExpandNode(undefined);
entityLineage, setTableColumns([]);
selectNodeHandler,
loadNodeHandler,
lineageLeafNodes,
isNodeLoading
) as Elements
);
}, [entityLineage, isNodeLoading]); }, [entityLineage, isNodeLoading]);
useEffect(() => {
onNodeExpand();
getTableColumns(expandNode);
}, [expandNode]);
useEffect(() => {
if (!isEmpty(selectedNode)) {
setExpandNode(undefined);
}
}, [selectedNode]);
useEffect(() => {
if (tableColumns.length) {
onNodeExpand(tableColumns);
}
}, [tableColumns]);
return ( return (
<div className="tw-relative tw-h-full tw--ml-4"> <div className="tw-relative tw-h-full tw--ml-4">
<div className="tw-w-full tw-h-full"> <div className="tw-w-full tw-h-full">
{(entityLineage?.downstreamEdges ?? []).length > 0 || {(entityLineage?.downstreamEdges ?? []).length > 0 ||
(entityLineage.upstreamEdges ?? []).length ? ( (entityLineage?.upstreamEdges ?? []).length > 0 ? (
<ReactFlowProvider> <ReactFlowProvider>
<ReactFlow <ReactFlow
panOnScroll panOnScroll
elements={elements as Elements} elements={elements as Elements}
nodesConnectable={false} nodesConnectable={false}
nodeTypes={{
output: CustomNode,
input: CustomNode,
default: CustomNode,
}}
onConnect={onConnect} onConnect={onConnect}
onElementClick={(_e, el) => onElementClick(el)} onElementClick={(_e, el) => onElementClick(el)}
onElementsRemove={onElementsRemove} onElementsRemove={onElementsRemove}
onLoad={onLoad} onLoad={onLoad}
onNodeContextMenu={onNodeContextMenu} onNodeContextMenu={onNodeContextMenu}
onNodeDrag={(e) => { onNodeDrag={dragHandle}
e.stopPropagation(); onNodeDragStart={dragHandle}
}} onNodeDragStop={dragHandle}
onNodeDragStart={(e) => {
e.stopPropagation();
}}
onNodeDragStop={(e) => {
e.stopPropagation();
}}
onNodeMouseEnter={onNodeMouseEnter} onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseLeave={onNodeMouseLeave} onNodeMouseLeave={onNodeMouseLeave}
onNodeMouseMove={onNodeMouseMove}> onNodeMouseMove={onNodeMouseMove}>
@ -465,21 +570,7 @@ const Entitylineage: FunctionComponent<EntityLineageProp> = ({
</ReactFlow> </ReactFlow>
</ReactFlowProvider> </ReactFlowProvider>
) : ( ) : (
<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"> getNoLineageDataPlaceholder()
<span>
Lineage is currently supported for Airflow. To enable lineage
collection from Airflow, Please follow the documentation
</span>
<Link
className="tw-ml-1"
target="_blank"
to={{
pathname:
'https://docs.open-metadata.org/install/metadata-ingestion/airflow/configure-airflow-lineage',
}}>
here
</Link>
</div>
)} )}
</div> </div>
<EntityInfoDrawer <EntityInfoDrawer

View File

@ -15,17 +15,20 @@
* limitations under the License. * limitations under the License.
*/ */
import classNames from 'classnames';
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import './Loader.css'; import './Loader.css';
type Props = { type Props = {
size?: 'default' | 'small'; size?: 'default' | 'small';
type?: 'default' | 'success' | 'error' | 'white'; type?: 'default' | 'success' | 'error' | 'white';
className?: string;
}; };
const Loader: FunctionComponent<Props> = ({ const Loader: FunctionComponent<Props> = ({
size = 'default', size = 'default',
type = 'default', type = 'default',
className = '',
}: Props): JSX.Element => { }: Props): JSX.Element => {
let classes = 'loader'; let classes = 'loader';
switch (size) { switch (size) {
@ -54,7 +57,9 @@ const Loader: FunctionComponent<Props> = ({
break; break;
} }
return <div className={classes} data-testid="loader" />; return (
<div className={classNames(classes, className)} data-testid="loader" />
);
}; };
export default Loader; export default Loader;

View File

@ -715,6 +715,9 @@ body .profiler-graph .recharts-active-dot circle {
} }
/* React flow */ /* React flow */
.react-flow__node {
min-width: max-content;
}
.leaf-node .react-flow__handle { .leaf-node .react-flow__handle {
background-color: #6b7280; background-color: #6b7280;

View File

@ -54,8 +54,10 @@ import IconInfo from '../assets/svg/info.svg';
import IconIngestion from '../assets/svg/ingestion.svg'; import IconIngestion from '../assets/svg/ingestion.svg';
import LogoMonogram from '../assets/svg/logo-monogram.svg'; import LogoMonogram from '../assets/svg/logo-monogram.svg';
import Logo from '../assets/svg/logo.svg'; import Logo from '../assets/svg/logo.svg';
import IconMinus from '../assets/svg/minus.svg';
import IconPipelineGrey from '../assets/svg/pipeline-grey.svg'; import IconPipelineGrey from '../assets/svg/pipeline-grey.svg';
import IconPipeline from '../assets/svg/pipeline.svg'; import IconPipeline from '../assets/svg/pipeline.svg';
import IconPlus from '../assets/svg/plus.svg';
import IconProfiler from '../assets/svg/profiler.svg'; import IconProfiler from '../assets/svg/profiler.svg';
import IconHelpCircle from '../assets/svg/question-circle.svg'; import IconHelpCircle from '../assets/svg/question-circle.svg';
import IconSetting from '../assets/svg/service.svg'; import IconSetting from '../assets/svg/service.svg';
@ -150,6 +152,8 @@ export const Icons = {
VERSION: 'icon-version', VERSION: 'icon-version',
VERSION_WHITE: 'icon-version-white', VERSION_WHITE: 'icon-version-white',
ICON_DEPLOY: 'icon-deploy', ICON_DEPLOY: 'icon-deploy',
ICON_PLUS: 'icon-plus',
ICON_MINUS: 'icon-minus',
}; };
const SVGIcons: FunctionComponent<Props> = ({ const SVGIcons: FunctionComponent<Props> = ({
@ -447,6 +451,14 @@ const SVGIcons: FunctionComponent<Props> = ({
case Icons.ICON_DEPLOY: case Icons.ICON_DEPLOY:
IconComponent = IconDeploy; IconComponent = IconDeploy;
break;
case Icons.ICON_PLUS:
IconComponent = IconPlus;
break;
case Icons.ICON_MINUS:
IconComponent = IconMinus;
break; break;
default: default: