-
Notifications
You must be signed in to change notification settings - Fork 432
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: display dependency graph on the "Resource Explorer" page (#1005)
* feat: add dependency graph backend (#883) * feat: add dependency graph frontend * feat: add filter state and share some of the inventory filter components with dependency graph --------- Co-authored-by: Avinesh Tripathi <73980067+AvineshTripathi@users.noreply.github.com> Co-authored-by: ShubhamPalriwala <spalriwalau@gmail.com> Co-authored-by: Mohamed Labouardy <mohamed@labouardy.com>
- Loading branch information
1 parent
5c7d07f
commit 7a3e8e0
Showing
47 changed files
with
5,799 additions
and
2,343 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,6 @@ | |
/node_modules | ||
/.pnp | ||
.pnp.js | ||
/package-lock.json | ||
|
||
# testing | ||
/coverage | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
nodejs 18.16.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/* eslint-disable react-hooks/exhaustive-deps */ | ||
import React, { useState, memo } from 'react'; | ||
import CytoscapeComponent from 'react-cytoscapejs'; | ||
import Cytoscape, { EventObject } from 'cytoscape'; | ||
|
||
import nodeHtmlLabel, { | ||
CytoscapeNodeHtmlParams | ||
// @ts-ignore | ||
} from 'cytoscape-node-html-label'; | ||
|
||
// @ts-ignore | ||
import COSEBilkent from 'cytoscape-cose-bilkent'; | ||
|
||
import { ReactFlowData } from './hooks/useDependencyGraph'; | ||
import { | ||
edgeAnimationConfig, | ||
edgeStyleConfig, | ||
graphLayoutConfig, | ||
leafStyleConfig, | ||
maxZoom, | ||
minZoom, | ||
nodeHTMLLabelConfig, | ||
nodeStyeConfig, | ||
zoomLevelBreakpoint | ||
} from './config'; | ||
|
||
export type DependencyGraphProps = { | ||
data: ReactFlowData; | ||
}; | ||
|
||
nodeHtmlLabel(Cytoscape.use(COSEBilkent)); | ||
const DependencyGraph = ({ data }: DependencyGraphProps) => { | ||
const [initDone, setInitDone] = useState(false); | ||
|
||
// Type technically is Cytoscape.EdgeCollection but that throws an unexpected error | ||
const loopAnimation = (eles: any) => { | ||
const ani = eles.animation(edgeAnimationConfig[0], edgeAnimationConfig[1]); | ||
|
||
ani | ||
.reverse() | ||
.play() | ||
.promise('complete') | ||
.then(() => loopAnimation(eles)); | ||
}; | ||
|
||
const cyActionHandlers = (cy: Cytoscape.Core) => { | ||
// make sure we did not init already, otherwise this will be bound more than once | ||
if (!initDone) { | ||
// Add HTML labels for better flexibility | ||
// @ts-ignore | ||
cy.nodeHtmlLabel([ | ||
{ | ||
...nodeHTMLLabelConfig, | ||
tpl(templateData: Cytoscape.NodeDataDefinition) { | ||
return `<div><p style="font-size: 10px; text-shadow: 0 0 5px #F4F9F9,0 0 5px #F4F9F9, | ||
0 0 5px #F4F9F9,0 0 5px #F4F9F9, | ||
0 0 5px #F4F9F9,0 0 5px #F4F9F9, | ||
0 0 5px #F4F9F9,0 0 5px #F4F9F9;" class="text-black-700 text-ellipsis max-w-[100px] overflow-hidden whitespace-nowrap text-center">${ | ||
templateData.label || ' ' | ||
}</p> | ||
<p style="font-size: 10px; text-shadow: 0 0 5px #F4F9F9,0 0 5px #F4F9F9, | ||
0 0 5px #F4F9F9,0 0 5px #F4F9F9, | ||
0 0 5px #F4F9F9,0 0 5px #F4F9F9, | ||
0 0 5px #F4F9F9,0 0 5px #F4F9F9;" class="text-black-400 text-ellipsis max-w-[100px] overflow-hidden whitespace-nowrap text-center font-thin">${ | ||
templateData.service || ' ' | ||
}</p></div>`; | ||
} | ||
} | ||
]); | ||
// Add class to leave nodes so we can make them smaller | ||
cy.nodes().leaves().addClass('leaf'); | ||
// same for root notes | ||
cy.nodes().roots().addClass('root'); | ||
// Animate edges | ||
cy.edges().forEach(loopAnimation); | ||
|
||
// Hide labels when being zoomed out | ||
cy.on('zoom', event => { | ||
const opacity = cy.zoom() <= zoomLevelBreakpoint ? 0 : 1; | ||
|
||
Array.from( | ||
document.querySelectorAll('.dependency-graph-node-label'), | ||
e => { | ||
// @ts-ignore | ||
e.style.opacity = opacity; | ||
return e; | ||
} | ||
); | ||
}); | ||
// Make sure to tell we inited successfully and prevent another init | ||
setInitDone(true); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="relative h-full flex-1 bg-dependency-graph bg-[length:40px_40px]"> | ||
<CytoscapeComponent | ||
className="h-full w-full" | ||
elements={CytoscapeComponent.normalizeElements({ | ||
nodes: data.nodes, | ||
edges: data.edges | ||
})} | ||
maxZoom={maxZoom} | ||
minZoom={minZoom} | ||
layout={graphLayoutConfig} | ||
stylesheet={[ | ||
{ | ||
selector: 'node', | ||
style: nodeStyeConfig | ||
}, | ||
{ | ||
selector: 'edge', | ||
style: edgeStyleConfig | ||
}, | ||
{ | ||
selector: '.leaf', | ||
style: leafStyleConfig | ||
} | ||
]} | ||
cy={(cy: Cytoscape.Core) => cyActionHandlers(cy)} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default memo(DependencyGraph); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import Button from '@components/button/Button'; | ||
|
||
type DashboardDependencyGraphErrorProps = { | ||
fetch: () => void; | ||
}; | ||
|
||
function DependencyGraphError({ fetch }: DashboardDependencyGraphErrorProps) { | ||
return ( | ||
<> | ||
<div className={`w-full rounded-lg bg-white px-6 py-4 pb-6`}> | ||
<div className="-mx-6 flex items-center justify-between border-b border-black-200/40 px-6 pb-4"> | ||
<div> | ||
<p className="text-sm font-semibold text-black-900"> | ||
Dependency Graph | ||
</p> | ||
<div className="mt-1"></div> | ||
<p className="text-xs text-black-300"> | ||
Analyze account resource associations | ||
</p> | ||
</div> | ||
<div className="flex h-[60px] items-center"></div> | ||
</div> | ||
<div className="mt-8"></div> | ||
<div className="flex flex-col items-center justify-center"> | ||
<svg | ||
className="h-20 w-20" | ||
fill="none" | ||
stroke="currentColor" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
strokeWidth={1.5} | ||
viewBox="0 0 24 24" | ||
> | ||
<path stroke="none" d="M0 0h24v24H0z" /> | ||
<path d="M4 8V6a2 2 0 012-2h2M4 16v2a2 2 0 002 2h2M16 4h2a2 2 0 012 2v2M16 20h2a2 2 0 002-2v-2M9 10h.01M15 10h.01M9.5 15.05a3.5 3.5 0 015 0" /> | ||
</svg> | ||
<p className="text-sm font-semibold text-black-900"> | ||
Cannot fetch Relationships | ||
</p> | ||
<div className="m-2 flex-shrink-0"> | ||
<Button style="secondary" size="sm" onClick={fetch}> | ||
<svg | ||
xmlns="http://www.w3.org/2000/svg" | ||
width="16" | ||
height="16" | ||
fill="none" | ||
viewBox="0 0 24 24" | ||
> | ||
<path | ||
stroke="currentColor" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
strokeWidth="1.5" | ||
d="M22 12c0 5.52-4.48 10-10 10s-8.89-5.56-8.89-5.56m0 0h4.52m-4.52 0v5M2 12C2 6.48 6.44 2 12 2c6.67 0 10 5.56 10 5.56m0 0v-5m0 5h-4.44" | ||
></path> | ||
</svg> | ||
Try again | ||
</Button> | ||
</div> | ||
</div> | ||
<div className="mt-12"></div> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
export default DependencyGraphError; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { memo } from 'react'; | ||
import DependencyGraphError from './DependencyGraphError'; | ||
import DependencyGraphSkeleton from './DependencyGraphSkeleton'; | ||
import DependencyGraphView from './DependencyGraph'; | ||
import { ReactFlowData } from './hooks/useDependencyGraph'; | ||
|
||
export type DependencyGraphLoaderProps = { | ||
loading: boolean; | ||
data: ReactFlowData | undefined; | ||
error: boolean; | ||
fetch: () => void; | ||
}; | ||
|
||
function DependencyGraphLoader({ | ||
loading, | ||
data, | ||
error, | ||
fetch | ||
}: DependencyGraphLoaderProps) { | ||
if (loading) return <DependencyGraphSkeleton />; | ||
|
||
if (error) return <DependencyGraphError fetch={fetch} />; | ||
|
||
if (data && !loading) return <DependencyGraphView data={data} />; | ||
|
||
return null; | ||
} | ||
|
||
export default memo(DependencyGraphLoader); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
function DependencyGraphSkeleton() { | ||
return ( | ||
<> | ||
<div | ||
data-testid="loading" | ||
className="relative flex h-full items-center justify-center bg-dependency-graph bg-[length:40px_40px] align-middle" | ||
> | ||
<div> | ||
<div className="h-3 w-24 rounded-lg bg-komiser-200/50"></div> | ||
<div className="mt-2"></div> | ||
<div className="h-3 w-48 rounded-lg bg-komiser-200/50"></div> | ||
</div> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
export default DependencyGraphSkeleton; |
107 changes: 107 additions & 0 deletions
107
dashboard/components/explorer/DependencyGraphWrapper.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { useRouter } from 'next/router'; | ||
import cn from 'classnames'; | ||
|
||
import { useEffect, useState } from 'react'; | ||
import parseURLParams from '@components/inventory/hooks/useInventory/helpers/parseURLParams'; | ||
import { InventoryFilterData } from '@components/inventory/hooks/useInventory/types/useInventoryTypes'; | ||
import ArrowDownIcon from '@components/icons/ArrowDownIcon'; | ||
import DependencyGraphLoader from './DependencyGraphLoader'; | ||
import DependendencyGraphFilter from './filter/DependendencyGraphFilter'; | ||
import useDependencyGraph from './hooks/useDependencyGraph'; | ||
|
||
function DependencyGraphWrapper() { | ||
const { | ||
loading, | ||
data, | ||
error, | ||
fetch, | ||
filters, | ||
displayedFilters, | ||
setDisplayedFilters, | ||
deleteFilter, | ||
setFilters | ||
} = useDependencyGraph(); | ||
const router = useRouter(); | ||
const [filterOpen, setFilterOpen] = useState(false); | ||
|
||
useEffect(() => { | ||
const newFilters: InventoryFilterData[] = Object.keys(router.query).map( | ||
param => parseURLParams(param as string, 'fetch') | ||
); | ||
const newFiltersToDisplay: InventoryFilterData[] = Object.keys( | ||
router.query | ||
).map(param => parseURLParams(param as string, 'display')); | ||
|
||
setFilters(newFilters); | ||
setDisplayedFilters(newFiltersToDisplay); | ||
}, [router.query]); | ||
|
||
useEffect(() => { | ||
const newFilters: InventoryFilterData[] = Object.keys(router.query).map( | ||
param => parseURLParams(param as string, 'fetch') | ||
); | ||
const newFiltersToDisplay: InventoryFilterData[] = Object.keys( | ||
router.query | ||
).map(param => parseURLParams(param as string, 'display')); | ||
|
||
setFilters(newFilters); | ||
setDisplayedFilters(newFiltersToDisplay); | ||
}, []); | ||
|
||
const hasFilters = | ||
Object.keys(router.query).length > 0 && | ||
displayedFilters && | ||
displayedFilters.length > 0; | ||
|
||
return ( | ||
<> | ||
<div className="flex h-[calc(100vh-145px)] w-full flex-col"> | ||
<div className="flex flex-row justify-between gap-2"> | ||
<p className="text-lg font-medium text-black-900">Graph View</p> | ||
<div | ||
className={cn( | ||
'absolute -top-1 right-24 z-20 flex translate-y-0 cursor-pointer items-center justify-start gap-4 rounded-b-[4px] border-x border-b border-black-170 bg-white px-4 py-2 text-sm transition', | ||
{ 'translate-y-[105px]': filterOpen } | ||
)} | ||
onClick={() => setFilterOpen(!filterOpen)} | ||
> | ||
{displayedFilters && displayedFilters?.length > 0 && ( | ||
<span className="bg-komiser-130 px-[6px] pb-[3px] pt-[2px] text-xs text-komiser-600"> | ||
{displayedFilters?.length} | ||
</span> | ||
)} | ||
<span className="">Filters</span> | ||
<ArrowDownIcon | ||
height="16" | ||
width="16" | ||
className={cn('transition', { | ||
'rotate-180': filterOpen | ||
})} | ||
/> | ||
</div> | ||
</div> | ||
<div | ||
className={cn( | ||
'absolute left-0 top-0 z-10 m-0 h-[102px] w-full origin-top scale-y-0 border-b border-black-170 bg-white px-24 transition', | ||
{ 'scale-y-100': filterOpen } | ||
)} | ||
> | ||
<DependendencyGraphFilter | ||
router={router} | ||
hasFilters={hasFilters} | ||
displayedFilters={displayedFilters} | ||
deleteFilter={deleteFilter} | ||
/> | ||
</div> | ||
<DependencyGraphLoader | ||
loading={loading} | ||
data={data} | ||
error={error} | ||
fetch={fetch} | ||
/> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
export default DependencyGraphWrapper; |
Oops, something went wrong.