Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: display dependency graph on the "Resource Explorer" page #1005

Merged
merged 43 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f91be16
Dependency Graph Backend (#883)
AvineshTripathi Jul 15, 2023
f51cf63
Added Dep graph section to contributing (#898)
AvineshTripathi Jul 15, 2023
c7bd27c
feat: Move to reactflow
Traxmaxx Sep 18, 2023
00f17e5
feat: use custom node template
Traxmaxx Sep 18, 2023
2fc9906
fix: fix node connector styling
Traxmaxx Sep 18, 2023
8ce153f
feat: switch to elkjs and import styling from OxGamma. Also use custo…
Traxmaxx Sep 19, 2023
25dc15e
feat: collect additional aws services connections
mlabouardy Sep 20, 2023
16995ce
Merge pull request #982 from tailwarden/feature/tech-1641
mlabouardy Sep 20, 2023
5e79fe5
fix: Update resourceId key for relations cleanup packages and fix labels
Traxmaxx Sep 20, 2023
aa3c3e5
chore: add .tools-version file for node version manager
Traxmaxx Sep 20, 2023
376304b
fix: move to cytoscape for the dependency graph
Traxmaxx Sep 20, 2023
5b2af61
fix: improve styling and add click listwnwe
Traxmaxx Sep 20, 2023
62c515a
feat: improve styling, reduce exsessive nesting, restructure components
Traxmaxx Sep 20, 2023
1fd2114
feat: improve styling and make leave nodes smaller
Traxmaxx Sep 20, 2023
43215e9
fix: fix image dimensions
Traxmaxx Sep 21, 2023
45dc945
fix: return resource's name
mlabouardy Sep 21, 2023
e2e7eeb
fix: hide leaves, edges and labels on zoom, try to get linear gradien…
Traxmaxx Sep 22, 2023
c8e77a0
feat: use math random for edge bezier
Traxmaxx Sep 22, 2023
e630778
feat: use math random for edge bezier and try to show labels during zoom
Traxmaxx Sep 22, 2023
66e61fb
show labels when zooming
Traxmaxx Sep 22, 2023
b331f55
chore: cleanup config and remove unused components
Traxmaxx Sep 25, 2023
91c4635
feat: support graph filters
mlabouardy Sep 25, 2023
fb031c8
Merge pull request #994 from tailwarden/feature/tech-1643
mlabouardy Sep 25, 2023
34a10ac
Merge branch 'develop' of github.com:tailwarden/komiser into wip-dep-…
Traxmaxx Sep 26, 2023
3942722
feat: add filter state and fix explorer layout
Traxmaxx Sep 26, 2023
106a57f
fix: temp rename due to MacOS case sensitivity issues
Traxmaxx Sep 26, 2023
70c7de6
fix: final rename due to MacOS case sensitivity issues
Traxmaxx Sep 26, 2023
d2aa5c7
fix: update filters and display current filter values
Traxmaxx Sep 26, 2023
8d40de1
fix: only display supported filters for dependency graph
Traxmaxx Sep 26, 2023
ee70f34
chore: remove duplication of nodes due to performance testing
Traxmaxx Sep 26, 2023
54689c4
Merge branch 'develop' of github.com:tailwarden/komiser into wip-dep-…
Traxmaxx Sep 27, 2023
55cda56
feat: move filter into toggled container and improve styling
Traxmaxx Sep 27, 2023
e8a7f56
chore: improve file structure
Traxmaxx Sep 27, 2023
61e3757
feat: size nodes based on weight aka connected edges aka degree
Traxmaxx Sep 27, 2023
129d836
fix: improve node sizing formula
Traxmaxx Sep 27, 2023
feebf27
Merge branch 'develop' of github.com:tailwarden/komiser into wip-dep-…
Traxmaxx Sep 27, 2023
52d4a99
chore: cleanup
Traxmaxx Sep 27, 2023
19c0ed8
chore: cleanup
Traxmaxx Sep 27, 2023
a11d3f6
chore: cleanup
Traxmaxx Sep 27, 2023
7c6f3e6
fix: readd package-lock.json because it should always be inside git
Traxmaxx Sep 27, 2023
a41fc91
fix: add ts-ignore for wrongly typed definitions
Traxmaxx Sep 27, 2023
2451188
Merge branch 'develop' of github.com:tailwarden/komiser into wip-dep-…
Traxmaxx Sep 27, 2023
c9e1d0c
Merge branch 'develop' into wip-dep-graph
ShubhamPalriwala Sep 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion dashboard/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
/node_modules
/.pnp
.pnp.js
/package-lock.json

# testing
/coverage
Expand Down
1 change: 1 addition & 0 deletions dashboard/.tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 18.16.1
126 changes: 126 additions & 0 deletions dashboard/components/explorer/DependencyGraph.tsx
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 || '&nbsp;'
}</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 || '&nbsp;'
}</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);
67 changes: 67 additions & 0 deletions dashboard/components/explorer/DependencyGraphError.tsx
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;
29 changes: 29 additions & 0 deletions dashboard/components/explorer/DependencyGraphLoader.tsx
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);
18 changes: 18 additions & 0 deletions dashboard/components/explorer/DependencyGraphSkeleton.tsx
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 dashboard/components/explorer/DependencyGraphWrapper.tsx
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;
Loading