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: add single dependency graph view for a specific resource relations #1161

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useRouter } from 'next/router';

import { InventoryFilterData } from '@components/inventory/hooks/useInventory/types/useInventoryTypes';
Expand Down Expand Up @@ -66,7 +66,7 @@ function GetData(res: any) {
return d;
}

function useDependencyGraph() {
function useDependencyGraph(resourceId?: string | null) {
shavidze marked this conversation as resolved.
Show resolved Hide resolved
const [loading, setLoading] = useState(true);
const [data, setData] = useState<ReactFlowData>();
const [error, setError] = useState(false);
Expand All @@ -76,25 +76,55 @@ function useDependencyGraph() {

const router = useRouter();

function fetch() {
const fetchRelationsByResourceId = useCallback(
(id: string) => {
settingsService
.getResourceRelations(id)
.then(res => {
if (res === Error) {
setLoading(false);
setError(true);
} else {
setLoading(false);
shavidze marked this conversation as resolved.
Show resolved Hide resolved
setData(GetData([].concat(res)));
}
})
.finally(() => {
setLoading(false);
});
},
[resourceId]
);

const fetchAllRelations = useCallback(() => {
settingsService
.getRelations(filters)
.then(res => {
if (res === Error) {
setLoading(false);
setError(true);
} else {
setLoading(false);
setData(GetData([].concat(res)));
}
})
.finally(() => {
setLoading(false);
});
}, [filters]);

const fetch = useCallback(() => {
if (!loading) {
setLoading(true);
}

if (error) {
setError(false);
}

settingsService.getRelations(filters).then(res => {
if (res === Error) {
setLoading(false);
setError(true);
} else {
setLoading(false);
setData(GetData(res));
}
});
}
if (resourceId) {
fetchRelationsByResourceId(resourceId);
} else fetchAllRelations();
}, []);

function deleteFilter(idx: number) {
const updatedFilters: InventoryFilterData[] = [...filters!];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import NumberInput from '@components/number-input/NumberInput';
import useInventory from '@components/inventory/hooks/useInventory/useInventory';
import settingsService from '@services/settingsService';
import InventorySidePanel from '@components/inventory/components/InventorySidePanel';
import { ReactFlowData } from './hooks/useDependencyGraph';
import { ReactFlowData } from '../hooks/useDependencyGraph';
import {
edgeAnimationConfig,
edgeStyleConfig,
Expand All @@ -33,7 +33,7 @@ import {
nodeStyeConfig,
// popperStyleConfig,
zoomLevelBreakpoint
} from './config';
} from '../config';

export type DependencyGraphProps = {
data: ReactFlowData;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { memo } from 'react';
import DependencyGraphError from './DependencyGraphError';
import DependencyGraphSkeleton from './DependencyGraphSkeleton';
import { ReactFlowData } from '../hooks/useDependencyGraph';
import DependencyGraphError from '../components/DependencyGraphError';
import DependencyGraphSkeleton from '../components/DependencyGraphSkeleton';
import DependencyGraphView from './DependencyGraph';
import { ReactFlowData } from './hooks/useDependencyGraph';

export type DependencyGraphLoaderProps = {
loading: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { InventoryFilterData } from '@components/inventory/hooks/useInventory/ty
import ArrowDownIcon from '@components/icons/ArrowDownIcon';
import EmptyState from '@components/empty-state/EmptyState';
import DependencyGraphLoader from './DependencyGraphLoader';
import DependendencyGraphFilter from './filter/DependendencyGraphFilter';
import useDependencyGraph from './hooks/useDependencyGraph';
import DependendencyGraphFilter from '../filter/DependendencyGraphFilter';
import useDependencyGraph from '../hooks/useDependencyGraph';

function DependencyGraphWrapper() {
const {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import React, { useState, memo } from 'react';
import CytoscapeComponent from 'react-cytoscapejs';
import Cytoscape, { EdgeSingular } from 'cytoscape';
import popper from 'cytoscape-popper';

import nodeHtmlLabel, {
CytoscapeNodeHtmlParams
// @ts-ignore
} from 'cytoscape-node-html-label';

// @ts-ignore
import COSEBilkent from 'cytoscape-cose-bilkent';

import EmptyState from '@components/empty-state/EmptyState';

import Tooltip from '@components/tooltip/Tooltip';
import WarningIcon from '@components/icons/WarningIcon';
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));
Cytoscape.use(popper);
const SingleDependencyGraph = ({ data }: DependencyGraphProps) => {
const [initDone, setInitDone] = useState(false);

const dataNodesLength: number = data.nodes.length;
// 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" title="${
templateData.label
}">${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" title="${
templateData.label
}">${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);

// Add hover tooltip on edges
cy.edges().bind('mouseover', event => {
if (cy.zoom() >= zoomLevelBreakpoint) {
// eslint-disable-next-line no-param-reassign
event.target.popperRefObj = event.target.popper({
content: () => {
const content = document.createElement('div');
content.classList.add('popper-div');
content.innerHTML = event.target.data('label');
content.style.pointerEvents = 'none';

document.body.appendChild(content);
return content;
}
});
}
});
// Hide Edges tooltip on mouseout
cy.edges().bind('mouseout', event => {
if (cy.zoom() >= zoomLevelBreakpoint && event.target.popperRefObj) {
event.target.popperRefObj.state.elements.popper.remove();
event.target.popperRefObj.destroy();
}
});

// Hide labels when being zoomed out
cy.on('zoom', event => {
if (cy.zoom() <= zoomLevelBreakpoint) {
interface ExtendedEdgeSingular extends EdgeSingular {
popperRefObj?: any;
}

// Check if a tooltip is present and remove it
cy.edges().forEach((edge: ExtendedEdgeSingular) => {
if (edge.popperRefObj) {
edge.popperRefObj.state.elements.popper.remove();
edge.popperRefObj.destroy();
}
});
}

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]">
{dataNodesLength === 0 ? (
<>
<div className="translate-y-[201px]">
<EmptyState
title="No results for this filter"
message="It seems like you have no cloud resources matching the filters you added"
mascotPose="tablet"
/>
</div>
</>
) : (
<>
<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 className="absolute bottom-0 left-0 flex gap-2 overflow-visible bg-black-100 text-black-400">
{dataNodesLength} {`related resource${dataNodesLength > 1 ? 's' : ''}`}
{dataNodesLength !== 0 && (
<div className="relative">
<WarningIcon className="peer" height="16" width="16" />
<Tooltip bottom="xs" align="left" width="lg">
Only AWS and Civo resources are currently supported on the
explorer.
</Tooltip>
</div>
)}
</div>
</div>
);
};

export default memo(SingleDependencyGraph);
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { memo } from 'react';
import DependencyGraphError from '../components/DependencyGraphError';
import DependencyGraphSkeleton from '../components/DependencyGraphSkeleton';
import { ReactFlowData } from '../hooks/useDependencyGraph';
import SingleDependencyGraphView from './SingleDependencyGraph';

export type SingleDependencyGraphLoaderProps = {
loading: boolean;
data: ReactFlowData | undefined;
error: boolean;
fetch: () => void;
};

function DependencyGraphLoader({
loading,
data,
error,
fetch
}: SingleDependencyGraphLoaderProps) {
if (loading) return <DependencyGraphSkeleton />;

if (error) return <DependencyGraphError fetch={fetch} />;

if (data && !loading) return <SingleDependencyGraphView data={data} />;

return null;
}

export default memo(DependencyGraphLoader);
Loading