From c812d7977303ed744e7d3d0e93ed8cb013c572c6 Mon Sep 17 00:00:00 2001 From: Saba Shavidze Date: Fri, 3 Nov 2023 17:26:59 +0400 Subject: [PATCH 1/9] feat: add single dependency graph view for a specific resource relations --- .../components}/DependencyGraphError.tsx | 0 .../components}/DependencyGraphSkeleton.tsx | 0 .../explorer/{ => dependency-graph}/config.ts | 0 .../filter/DependencyGraphFilterDropdown.tsx | 0 .../filter/DependencyGraphFilterField.tsx | 0 .../filter/DependencyGraphFilterOptions.tsx | 0 .../filter/DependencyGraphFilterSummary.tsx | 0 .../filter/DependendencyGraphFilter.tsx | 0 .../hooks/useDependencyGraph.tsx | 58 ++++-- .../DependencyGraph.tsx | 4 +- .../DependencyGraphLoader.tsx | 6 +- .../DependencyGraphWrapper.tsx | 4 +- .../SingleDependencyGraph.tsx | 197 ++++++++++++++++++ .../SingleDependencyGraphLoader.tsx | 29 +++ .../SingleDependencyGraphWrapper.tsx | 59 ++++++ .../components/InventorySidePanel.tsx | 35 +++- .../OnboardingWizardLayout.tsx | 6 +- .../components/sidepanel/SidepanelHeader.tsx | 8 +- .../components/sidepanel/SidepanelPage.tsx | 2 +- dashboard/pages/dashboard.tsx | 1 - dashboard/pages/explorer.tsx | 2 +- dashboard/pages/inventory.tsx | 2 +- dashboard/services/settingsService.ts | 9 + 23 files changed, 378 insertions(+), 44 deletions(-) rename dashboard/components/explorer/{ => dependency-graph/components}/DependencyGraphError.tsx (100%) rename dashboard/components/explorer/{ => dependency-graph/components}/DependencyGraphSkeleton.tsx (100%) rename dashboard/components/explorer/{ => dependency-graph}/config.ts (100%) rename dashboard/components/explorer/{ => dependency-graph}/filter/DependencyGraphFilterDropdown.tsx (100%) rename dashboard/components/explorer/{ => dependency-graph}/filter/DependencyGraphFilterField.tsx (100%) rename dashboard/components/explorer/{ => dependency-graph}/filter/DependencyGraphFilterOptions.tsx (100%) rename dashboard/components/explorer/{ => dependency-graph}/filter/DependencyGraphFilterSummary.tsx (100%) rename dashboard/components/explorer/{ => dependency-graph}/filter/DependendencyGraphFilter.tsx (100%) rename dashboard/components/explorer/{ => dependency-graph}/hooks/useDependencyGraph.tsx (74%) rename dashboard/components/explorer/{ => dependency-graph/multi-resource-dependency-graph}/DependencyGraph.tsx (98%) rename dashboard/components/explorer/{ => dependency-graph/multi-resource-dependency-graph}/DependencyGraphLoader.tsx (73%) rename dashboard/components/explorer/{ => dependency-graph/multi-resource-dependency-graph}/DependencyGraphWrapper.tsx (96%) create mode 100644 dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraph.tsx create mode 100644 dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphLoader.tsx create mode 100644 dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphWrapper.tsx diff --git a/dashboard/components/explorer/DependencyGraphError.tsx b/dashboard/components/explorer/dependency-graph/components/DependencyGraphError.tsx similarity index 100% rename from dashboard/components/explorer/DependencyGraphError.tsx rename to dashboard/components/explorer/dependency-graph/components/DependencyGraphError.tsx diff --git a/dashboard/components/explorer/DependencyGraphSkeleton.tsx b/dashboard/components/explorer/dependency-graph/components/DependencyGraphSkeleton.tsx similarity index 100% rename from dashboard/components/explorer/DependencyGraphSkeleton.tsx rename to dashboard/components/explorer/dependency-graph/components/DependencyGraphSkeleton.tsx diff --git a/dashboard/components/explorer/config.ts b/dashboard/components/explorer/dependency-graph/config.ts similarity index 100% rename from dashboard/components/explorer/config.ts rename to dashboard/components/explorer/dependency-graph/config.ts diff --git a/dashboard/components/explorer/filter/DependencyGraphFilterDropdown.tsx b/dashboard/components/explorer/dependency-graph/filter/DependencyGraphFilterDropdown.tsx similarity index 100% rename from dashboard/components/explorer/filter/DependencyGraphFilterDropdown.tsx rename to dashboard/components/explorer/dependency-graph/filter/DependencyGraphFilterDropdown.tsx diff --git a/dashboard/components/explorer/filter/DependencyGraphFilterField.tsx b/dashboard/components/explorer/dependency-graph/filter/DependencyGraphFilterField.tsx similarity index 100% rename from dashboard/components/explorer/filter/DependencyGraphFilterField.tsx rename to dashboard/components/explorer/dependency-graph/filter/DependencyGraphFilterField.tsx diff --git a/dashboard/components/explorer/filter/DependencyGraphFilterOptions.tsx b/dashboard/components/explorer/dependency-graph/filter/DependencyGraphFilterOptions.tsx similarity index 100% rename from dashboard/components/explorer/filter/DependencyGraphFilterOptions.tsx rename to dashboard/components/explorer/dependency-graph/filter/DependencyGraphFilterOptions.tsx diff --git a/dashboard/components/explorer/filter/DependencyGraphFilterSummary.tsx b/dashboard/components/explorer/dependency-graph/filter/DependencyGraphFilterSummary.tsx similarity index 100% rename from dashboard/components/explorer/filter/DependencyGraphFilterSummary.tsx rename to dashboard/components/explorer/dependency-graph/filter/DependencyGraphFilterSummary.tsx diff --git a/dashboard/components/explorer/filter/DependendencyGraphFilter.tsx b/dashboard/components/explorer/dependency-graph/filter/DependendencyGraphFilter.tsx similarity index 100% rename from dashboard/components/explorer/filter/DependendencyGraphFilter.tsx rename to dashboard/components/explorer/dependency-graph/filter/DependendencyGraphFilter.tsx diff --git a/dashboard/components/explorer/hooks/useDependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraph.tsx similarity index 74% rename from dashboard/components/explorer/hooks/useDependencyGraph.tsx rename to dashboard/components/explorer/dependency-graph/hooks/useDependencyGraph.tsx index 265c63404..defb743cd 100644 --- a/dashboard/components/explorer/hooks/useDependencyGraph.tsx +++ b/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraph.tsx @@ -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'; @@ -66,7 +66,7 @@ function GetData(res: any) { return d; } -function useDependencyGraph() { +function useDependencyGraph(resourceId?: string | null) { const [loading, setLoading] = useState(true); const [data, setData] = useState(); const [error, setError] = useState(false); @@ -76,7 +76,44 @@ 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); + 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); } @@ -84,17 +121,10 @@ function useDependencyGraph() { 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!]; diff --git a/dashboard/components/explorer/DependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx similarity index 98% rename from dashboard/components/explorer/DependencyGraph.tsx rename to dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx index 981cb219c..017dbf75d 100644 --- a/dashboard/components/explorer/DependencyGraph.tsx +++ b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx @@ -19,7 +19,7 @@ import WarningIcon from '@components/icons/WarningIcon'; 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, @@ -30,7 +30,7 @@ import { nodeHTMLLabelConfig, nodeStyeConfig, zoomLevelBreakpoint -} from './config'; +} from '../config'; export type DependencyGraphProps = { data: ReactFlowData; diff --git a/dashboard/components/explorer/DependencyGraphLoader.tsx b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphLoader.tsx similarity index 73% rename from dashboard/components/explorer/DependencyGraphLoader.tsx rename to dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphLoader.tsx index 1e64707e7..c575a780e 100644 --- a/dashboard/components/explorer/DependencyGraphLoader.tsx +++ b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphLoader.tsx @@ -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; diff --git a/dashboard/components/explorer/DependencyGraphWrapper.tsx b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphWrapper.tsx similarity index 96% rename from dashboard/components/explorer/DependencyGraphWrapper.tsx rename to dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphWrapper.tsx index 27034d0ed..7eaab2ded 100644 --- a/dashboard/components/explorer/DependencyGraphWrapper.tsx +++ b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphWrapper.tsx @@ -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 { diff --git a/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraph.tsx new file mode 100644 index 000000000..71623af35 --- /dev/null +++ b/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraph.tsx @@ -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 `

${templateData.label || ' '}

+

${templateData.service || ' '}

`; + } + } + ]); + // 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 ( +
+ {dataNodesLength === 0 ? ( + <> +
+ +
+ + ) : ( + <> + cyActionHandlers(cy)} + /> + + )} +
+ {dataNodesLength} {`related resource${dataNodesLength > 1 ? 's' : ''}`} + {dataNodesLength !== 0 && ( +
+ + + Only AWS and Civo resources are currently supported on the + explorer. + +
+ )} +
+
+ ); +}; + +export default memo(SingleDependencyGraph); diff --git a/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphLoader.tsx b/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphLoader.tsx new file mode 100644 index 000000000..d189df6e4 --- /dev/null +++ b/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphLoader.tsx @@ -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 ; + + if (error) return ; + + if (data && !loading) return ; + + return null; +} + +export default memo(DependencyGraphLoader); diff --git a/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphWrapper.tsx b/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphWrapper.tsx new file mode 100644 index 000000000..7e606d855 --- /dev/null +++ b/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphWrapper.tsx @@ -0,0 +1,59 @@ +import { useRouter } from 'next/router'; + +import EmptyState from '@components/empty-state/EmptyState'; +import HyperLinkIcon from '@components/icons/HyperLinkIcon'; +import useDependencyGraph from '../hooks/useDependencyGraph'; +import SingleDependencyGraphLoader from './SingleDependencyGraphLoader'; + +function SingleDependencyGraphWrapper({ resourceId }: { resourceId?: string }) { + const { loading, data, error, fetch } = useDependencyGraph(resourceId); + const router = useRouter(); + const title = 'Open in explorer'; + return ( + <> +
+ + {!data?.nodes.length && !data?.edges.length ? ( +
+ { + router.push( + 'https://github.com/tailwarden/komiser/issues/new/choose' + ); + }} + action={() => { + router.push('/'); + }} + /> +
+ ) : ( + + )} +
+ + ); +} + +export default SingleDependencyGraphWrapper; diff --git a/dashboard/components/inventory/components/InventorySidePanel.tsx b/dashboard/components/inventory/components/InventorySidePanel.tsx index 8248657cd..fe0fddca6 100644 --- a/dashboard/components/inventory/components/InventorySidePanel.tsx +++ b/dashboard/components/inventory/components/InventorySidePanel.tsx @@ -1,6 +1,7 @@ import SidepanelHeader from '@components/sidepanel/SidepanelHeader'; import SidepanelPage from '@components/sidepanel/SidepanelPage'; import Pill from '@components/pill/Pill'; +import SingleDependencyGraphWrapper from '@components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphWrapper'; import formatNumber from '../../../utils/formatNumber'; import providers from '../../../utils/providerHelper'; import Button from '../../button/Button'; @@ -53,8 +54,16 @@ function InventorySidePanel({ const getLastFetched = (date: string) => { const dateLastFetched = new Date(date); const today = new Date(); - const aMonthAgo = new Date(today.getFullYear(), today.getMonth() - 1, today.getDate()); - const aWeekAgo = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 7); + const aMonthAgo = new Date( + today.getFullYear(), + today.getMonth() - 1, + today.getDate() + ); + const aWeekAgo = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() - 7 + ); let message; if (dateLastFetched > aMonthAgo) { message = 'Since last month'; @@ -101,10 +110,10 @@ function InventorySidePanel({
-

+

Cloud account

-

+

{!data && (

)} @@ -112,10 +121,10 @@ function InventorySidePanel({

-

+

Region

-

+

{!data && (

)} @@ -123,7 +132,7 @@ function InventorySidePanel({

-

+

Cost

@@ -139,10 +148,10 @@ function InventorySidePanel({

-

+

Relations

-

+

{!data && (

)} @@ -154,6 +163,7 @@ function InventorySidePanel({

)} + {/* Tags form */} {tabs.includes('tags') && ( @@ -227,6 +237,13 @@ function InventorySidePanel({ )} + + {/* Relations */} + {tabs.includes('relations') && ( + + + + )}
{page === 'delete' && ( <> diff --git a/dashboard/components/onboarding-wizard/OnboardingWizardLayout.tsx b/dashboard/components/onboarding-wizard/OnboardingWizardLayout.tsx index 1b2d9de58..1becc0279 100644 --- a/dashboard/components/onboarding-wizard/OnboardingWizardLayout.tsx +++ b/dashboard/components/onboarding-wizard/OnboardingWizardLayout.tsx @@ -4,11 +4,7 @@ import OnboardingWizardHeader from './PageHeaders'; import OnboardingWizardProgressBar from './ProgressBar'; function OnboardingWizardLayout({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); + return
{children}
; } type LeftSideLayoutProps = { diff --git a/dashboard/components/sidepanel/SidepanelHeader.tsx b/dashboard/components/sidepanel/SidepanelHeader.tsx index c4832ff06..9fb8f715b 100644 --- a/dashboard/components/sidepanel/SidepanelHeader.tsx +++ b/dashboard/components/sidepanel/SidepanelHeader.tsx @@ -46,7 +46,7 @@ function SidepanelHeader({ )}
-

+

{title}

-

- {subtitle} -

+

{subtitle}

)} @@ -70,7 +68,7 @@ function SidepanelHeader({
-

+

{title}

diff --git a/dashboard/components/sidepanel/SidepanelPage.tsx b/dashboard/components/sidepanel/SidepanelPage.tsx index 124fbe6e3..cb989c820 100644 --- a/dashboard/components/sidepanel/SidepanelPage.tsx +++ b/dashboard/components/sidepanel/SidepanelPage.tsx @@ -21,7 +21,7 @@ function SidepanelPage({ container ? 'rounded-lg bg-black-100 p-6' : ' h-full overflow-auto' } > -
{children}
+
{children}
)} diff --git a/dashboard/pages/dashboard.tsx b/dashboard/pages/dashboard.tsx index e4d185cc9..285e2e48f 100644 --- a/dashboard/pages/dashboard.tsx +++ b/dashboard/pages/dashboard.tsx @@ -5,7 +5,6 @@ import DashboardLayout from '../components/dashboard/components/DashboardLayout' import DashboardResourcesManager from '../components/dashboard/components/resources-manager/DashboardResourcesManager'; import DashboardTopStats from '../components/dashboard/components/top-stats/DashboardTopStats'; import Grid from '../components/grid/Grid'; -import DashboardDependencyGraphWrapper from '../components/explorer/DependencyGraphWrapper'; function Dashboard() { return ( diff --git a/dashboard/pages/explorer.tsx b/dashboard/pages/explorer.tsx index a6bf6152d..277f9c261 100644 --- a/dashboard/pages/explorer.tsx +++ b/dashboard/pages/explorer.tsx @@ -1,5 +1,5 @@ import Head from 'next/head'; -import DashboardDependencyGraphWrapper from '../components/explorer/DependencyGraphWrapper'; +import DashboardDependencyGraphWrapper from '../components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphWrapper'; function Explorer() { return ( diff --git a/dashboard/pages/inventory.tsx b/dashboard/pages/inventory.tsx index 4ef3017a4..d4917be34 100644 --- a/dashboard/pages/inventory.tsx +++ b/dashboard/pages/inventory.tsx @@ -173,7 +173,7 @@ export default function Inventory() { deleteLoading={deleteLoading} bulkItems={bulkItems} updateBulkTags={updateBulkTags} - tabs={['resource details', 'tags']} + tabs={['resource details', 'tags', 'relations']} /> {/* Error state */} diff --git a/dashboard/services/settingsService.ts b/dashboard/services/settingsService.ts index 59d48e463..6d8a0a23c 100644 --- a/dashboard/services/settingsService.ts +++ b/dashboard/services/settingsService.ts @@ -96,6 +96,15 @@ const settingsService = { } }, + async getResourceRelations(resourceId: string) { + try { + const res = await fetch(`${BASE_URL}/resources?resourceId=${resourceId}`); + const data = await res.json(); + return data; + } catch (error) { + return Error; + } + }, async getGlobalResources(payload: string) { try { const res = await fetch( From 0c170e46a845efacc031d04fb499a98ee486e875 Mon Sep 17 00:00:00 2001 From: Saba Shavidze Date: Sat, 4 Nov 2023 12:42:49 +0400 Subject: [PATCH 2/9] fix: update folder name for dependency graph --- .../SingleDependencyGraph.tsx | 0 .../SingleDependencyGraphLoader.tsx | 0 .../SingleDependencyGraphWrapper.tsx | 0 .../components/inventory/components/InventorySidePanel.tsx | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename dashboard/components/explorer/dependency-graph/{single-resource-graph-dependency-graph => single-resource-dependency-graph}/SingleDependencyGraph.tsx (100%) rename dashboard/components/explorer/dependency-graph/{single-resource-graph-dependency-graph => single-resource-dependency-graph}/SingleDependencyGraphLoader.tsx (100%) rename dashboard/components/explorer/dependency-graph/{single-resource-graph-dependency-graph => single-resource-dependency-graph}/SingleDependencyGraphWrapper.tsx (100%) diff --git a/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx similarity index 100% rename from dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraph.tsx rename to dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx diff --git a/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphLoader.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphLoader.tsx similarity index 100% rename from dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphLoader.tsx rename to dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphLoader.tsx diff --git a/dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphWrapper.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx similarity index 100% rename from dashboard/components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphWrapper.tsx rename to dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx diff --git a/dashboard/components/inventory/components/InventorySidePanel.tsx b/dashboard/components/inventory/components/InventorySidePanel.tsx index 73d28156e..2070b8c00 100644 --- a/dashboard/components/inventory/components/InventorySidePanel.tsx +++ b/dashboard/components/inventory/components/InventorySidePanel.tsx @@ -1,7 +1,7 @@ import SidepanelHeader from '@components/sidepanel/SidepanelHeader'; import SidepanelPage from '@components/sidepanel/SidepanelPage'; import Pill from '@components/pill/Pill'; -import SingleDependencyGraphWrapper from '@components/explorer/dependency-graph/single-resource-graph-dependency-graph/SingleDependencyGraphWrapper'; +import SingleDependencyGraphWrapper from '@components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper'; import Button from '@components/button/Button'; import formatNumber from '../../../utils/formatNumber'; import CloseIcon from '../../icons/CloseIcon'; From 06313637a38d18d9004fe5af30536b574269f747 Mon Sep 17 00:00:00 2001 From: Saba Shavidze Date: Mon, 6 Nov 2023 12:17:56 +0400 Subject: [PATCH 3/9] fix: update pull request based on review and seperate actions logic in hook --- .../hooks/useDependencyGraph.tsx | 9 +- .../hooks/useDependencyGraphActions.tsx | 213 ++++++++++++++++++ .../DependencyGraph.tsx | 208 ++--------------- .../SingleDependencyGraph.tsx | 189 +++++----------- .../components/InventorySidePanel.tsx | 10 +- dashboard/services/settingsService.ts | 10 - dashboard/styles/globals.css | 10 +- 7 files changed, 296 insertions(+), 353 deletions(-) create mode 100644 dashboard/components/explorer/dependency-graph/hooks/useDependencyGraphActions.tsx diff --git a/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraph.tsx index defb743cd..2642254bb 100644 --- a/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraph.tsx +++ b/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraph.tsx @@ -9,6 +9,10 @@ export type ReactFlowData = { edges: any[]; }; +export type DependencyGraphProps = { + data: ReactFlowData; +}; + // converting the json object into data that reactflow needs // TODO - based on selected library function GetData(res: any) { @@ -66,7 +70,7 @@ function GetData(res: any) { return d; } -function useDependencyGraph(resourceId?: string | null) { +function useDependencyGraph(resourceId?: string) { const [loading, setLoading] = useState(true); const [data, setData] = useState(); const [error, setError] = useState(false); @@ -75,11 +79,10 @@ function useDependencyGraph(resourceId?: string | null) { useState(); const router = useRouter(); - const fetchRelationsByResourceId = useCallback( (id: string) => { settingsService - .getResourceRelations(id) + .getResourceById(`?resourceId=${id}`) .then(res => { if (res === Error) { setLoading(false); diff --git a/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraphActions.tsx b/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraphActions.tsx new file mode 100644 index 000000000..e69dd776f --- /dev/null +++ b/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraphActions.tsx @@ -0,0 +1,213 @@ +// useDependencyGraphActions.js +import { useEffect, useRef, useState } from 'react'; +import Cytoscape, { EdgeSingular, EventObject } 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 settingsService from '@services/settingsService'; +import useInventory from '@components/inventory/hooks/useInventory/useInventory'; +import { + edgeAnimationConfig, + maxZoom, + minZoom, + nodeHTMLLabelConfig, + zoomLevelBreakpoint +} from '../config'; + +type UseDependencyGraphActionsT = { + enableClickHandler?: boolean; +}; + +// Register the extensions only once +nodeHtmlLabel(Cytoscape.use(COSEBilkent)); +Cytoscape.use(popper); + +export const useDependencyGraphActions = ({ + enableClickHandler = true +}: UseDependencyGraphActionsT) => { + const [initDone, setInitDone] = useState(false); + + const [isNodeDraggingEnabled, setNodeDraggingEnabled] = useState(true); + + const [zoomLevel, setZoomLevel] = useState(minZoom); + const [zoomVal, setZoomVal] = useState(0); // debounced zoom state to display percentage + const cyRef = useRef(null); + + const { openModal } = useInventory(); + + // opens modal to display details of clicked node + const handleNodeClick = async (event: EventObject) => { + const nodeData = event.target.data(); + settingsService.getResourceById(`?resourceId=${nodeData.id}`).then(res => { + if (res !== Error) { + openModal(res); + } + }); + }; + + 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) => { + if (!initDone) { + // Add HTML labels for better flexibility + // @ts-ignore + cy.nodeHtmlLabel([ + { + ...nodeHTMLLabelConfig, + tpl: ( + templateData: Cytoscape.NodeDataDefinition + ) => `

${templateData.label || ' '}

+

${templateData.service || ' '}

` + } + ]); + + // Add class to leave nodes so we can make them smaller + cy.nodes().leaves().addClass('leaf'); + // same for root nodes + cy.nodes().roots().addClass('root'); + // Animate edges + cy.edges().forEach(loopAnimation); + + // Add a click event listener to the Cytoscape graph + if (enableClickHandler) { + cy.on('tap', 'node', handleNodeClick); + } + + // 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 => { + const newZoomLevel = event.cy.zoom(); + // setZoomLevel(newZoomLevel); + + if (newZoomLevel <= 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(); + } + }); + } + + // update state with new zoom level + setZoomLevel(newZoomLevel); + + const opacity = cy.zoom() <= zoomLevelBreakpoint ? 0 : 1; + + Array.from( + document.querySelectorAll('.dependency-graph-nodeLabel'), + e => { + // @ts-ignore + e.style.opacity = opacity; + return e; + } + ); + }); + // Make sure to tell we inited successfully and prevent another init + setInitDone(true); + } + }; + + const handleZoomChange = (zoomPercentage: number) => { + let newZoomLevel = minZoom + zoomPercentage * ((maxZoom - minZoom) / 100); + if (newZoomLevel < minZoom) newZoomLevel = minZoom; + if (newZoomLevel > maxZoom) newZoomLevel = maxZoom; + if (cyRef.current) { + cyRef.current.zoom(newZoomLevel); + setZoomLevel(newZoomLevel); + } + }; + + const toggleNodeDragging = () => { + if (cyRef.current) { + if (isNodeDraggingEnabled) { + // to disable node dragging in Cytoscape + cyRef.current.nodes().ungrabify(); + } else { + // to enable node dragging in Cytoscape + cyRef.current.nodes().grabify(); + } + setNodeDraggingEnabled(!isNodeDraggingEnabled); + } + }; + + let translateXClass; + + if (zoomVal < 10) { + translateXClass = 'translate-x-1'; + } else if (zoomVal >= 10 && zoomVal < 100) { + translateXClass = 'translate-x-2'; + } else { + translateXClass = 'translate-x-3'; + } + useEffect(() => { + const zoomPercentage = Math.round( + ((zoomLevel - minZoom) / (maxZoom - minZoom)) * 100 + ); + const handler = setTimeout(() => { + setZoomVal(zoomPercentage); + }, 100); // 100ms debounce + return () => { + clearTimeout(handler); + }; + }, [zoomLevel]); + + return { + cyRef, + cyActionHandlers, + toggleNodeDragging, + isNodeDraggingEnabled, + handleZoomChange, + zoomVal, + translateXClass + }; +}; diff --git a/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx index 62bec3bfe..5315dd453 100644 --- a/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx +++ b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx @@ -1,16 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import React, { useState, memo, useEffect, useRef } from 'react'; +import React, { memo } from 'react'; import CytoscapeComponent from 'react-cytoscapejs'; -import Cytoscape, { EdgeSingular, EventObject } 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 Cytoscape from 'cytoscape'; import EmptyState from '@components/empty-state/EmptyState'; @@ -19,40 +10,31 @@ import WarningIcon from '@components/icons/WarningIcon'; import DragIcon from '@components/icons/DragIcon'; 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 { DependencyGraphProps } from '../hooks/useDependencyGraph'; import { - edgeAnimationConfig, edgeStyleConfig, graphLayoutConfig, leafStyleConfig, maxZoom, minZoom, - nodeHTMLLabelConfig, - nodeStyeConfig, - // popperStyleConfig, - zoomLevelBreakpoint + nodeStyeConfig } from '../config'; +import { useDependencyGraphActions } from '../hooks/useDependencyGraphActions'; -export type DependencyGraphProps = { - data: ReactFlowData; -}; - -nodeHtmlLabel(Cytoscape.use(COSEBilkent)); -Cytoscape.use(popper); const DependencyGraph = ({ data }: DependencyGraphProps) => { - const [initDone, setInitDone] = useState(false); const dataIsEmpty: boolean = data.nodes.length === 0; + const { + cyRef, + cyActionHandlers, + toggleNodeDragging, + isNodeDraggingEnabled, + translateXClass, + zoomVal, + handleZoomChange + } = useDependencyGraphActions({ enableClickHandler: true }); - const [zoomLevel, setZoomLevel] = useState(minZoom); - const [zoomVal, setZoomVal] = useState(0); // debounced zoom state to display percentage - - const [isNodeDraggingEnabled, setNodeDraggingEnabled] = useState(true); - - const cyRef = useRef(null); const { - openModal, isOpen, closeModal, data: inventoryItem, @@ -69,168 +51,6 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => { updateBulkTags } = useInventory(); - // opens modal to display details of clicked node - const handleNodeClick = async (event: EventObject) => { - const nodeData = event.target.data(); - settingsService.getResourceById(`?resourceId=${nodeData.id}`).then(res => { - if (res !== Error) { - openModal(res); - } - }); - }; - - // 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 `

${templateData.label || ' '}

-

${templateData.service || ' '}

`; - } - } - ]); - // 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 a click event listener to the Cytoscape graph - cy.on('tap', 'node', handleNodeClick); - - // 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 => { - const newZoomLevel = event.cy.zoom(); - // setZoomLevel(newZoomLevel); - - if (newZoomLevel <= 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(); - } - }); - } - - // update state with new zoom level - setZoomLevel(newZoomLevel); - - const opacity = cy.zoom() <= zoomLevelBreakpoint ? 0 : 1; - - Array.from( - document.querySelectorAll('.dependency-graph-nodeLabel'), - e => { - // @ts-ignore - e.style.opacity = opacity; - return e; - } - ); - }); - // Make sure to tell we inited successfully and prevent another init - setInitDone(true); - } - }; - - useEffect(() => { - const zoomPercentage = Math.round( - ((zoomLevel - minZoom) / (maxZoom - minZoom)) * 100 - ); - const handler = setTimeout(() => { - setZoomVal(zoomPercentage); - }, 100); // 100ms debounce - return () => { - clearTimeout(handler); - }; - }, [zoomLevel]); - - const toggleNodeDragging = () => { - if (cyRef.current) { - if (isNodeDraggingEnabled) { - // to disable node dragging in Cytoscape - cyRef.current.nodes().ungrabify(); - } else { - // to enable node dragging in Cytoscape - cyRef.current.nodes().grabify(); - } - setNodeDraggingEnabled(!isNodeDraggingEnabled); - } - }; - - const handleZoomChange = (zoomPercentage: number) => { - let newZoomLevel = minZoom + zoomPercentage * ((maxZoom - minZoom) / 100); - if (newZoomLevel < minZoom) newZoomLevel = minZoom; - if (newZoomLevel > maxZoom) newZoomLevel = maxZoom; - if (cyRef.current) { - cyRef.current.zoom(newZoomLevel); - setZoomLevel(newZoomLevel); - } - }; - - let translateXClass; - - if (zoomVal < 10) { - translateXClass = 'translate-x-1'; - } else if (zoomVal >= 10 && zoomVal < 100) { - translateXClass = 'translate-x-2'; - } else { - translateXClass = 'translate-x-3'; - } - return (
{dataIsEmpty ? ( diff --git a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx index 71623af35..7a7af5197 100644 --- a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx +++ b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx @@ -1,141 +1,34 @@ -import React, { useState, memo } from 'react'; +import React, { 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 Cytoscape from 'cytoscape'; 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 { DragIcon } from '@components/icons'; +import NumberInput from '@components/number-input/NumberInput'; +import { DependencyGraphProps } from '../hooks/useDependencyGraph'; import { - edgeAnimationConfig, edgeStyleConfig, graphLayoutConfig, leafStyleConfig, maxZoom, minZoom, - nodeHTMLLabelConfig, - nodeStyeConfig, - zoomLevelBreakpoint + nodeStyeConfig } from '../config'; +import { useDependencyGraphActions } from '../hooks/useDependencyGraphActions'; -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 `

${templateData.label || ' '}

-

${templateData.service || ' '}

`; - } - } - ]); - // 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); - } - }; + const { + cyRef, + cyActionHandlers, + toggleNodeDragging, + isNodeDraggingEnabled, + translateXClass, + zoomVal, + handleZoomChange + } = useDependencyGraphActions({ enableClickHandler: false }); return (
@@ -159,6 +52,7 @@ const SingleDependencyGraph = ({ data }: DependencyGraphProps) => { })} maxZoom={maxZoom} minZoom={minZoom} + zoom={(minZoom + maxZoom) / 2} layout={graphLayoutConfig} stylesheet={[ { @@ -174,21 +68,50 @@ const SingleDependencyGraph = ({ data }: DependencyGraphProps) => { style: leafStyleConfig } ]} - cy={(cy: Cytoscape.Core) => cyActionHandlers(cy)} + cy={(cy: Cytoscape.Core) => { + cyActionHandlers(cy); + cyRef.current = cy; + }} /> )} -
- {dataNodesLength} {`related resource${dataNodesLength > 1 ? 's' : ''}`} - {dataNodesLength !== 0 && ( -
- - - Only AWS and Civo resources are currently supported on the - explorer. +
+
+
+ {dataNodesLength} {`Resource${dataNodesLength > 1 ? 's' : ''}`} +
+
+ + + {isNodeDraggingEnabled + ? 'Disable node dragging' + : 'Enable node dragging'} + +
+ handleZoomChange(Number(zoomData.zoom))} + handleValueChange={handleZoomChange} // increment or decrement input value + step={5} // percentage change in zoom + maxLength={3} + /> + + % + +
- )} +
); diff --git a/dashboard/components/inventory/components/InventorySidePanel.tsx b/dashboard/components/inventory/components/InventorySidePanel.tsx index 2070b8c00..b6ae3a6f5 100644 --- a/dashboard/components/inventory/components/InventorySidePanel.tsx +++ b/dashboard/components/inventory/components/InventorySidePanel.tsx @@ -3,11 +3,11 @@ import SidepanelPage from '@components/sidepanel/SidepanelPage'; import Pill from '@components/pill/Pill'; import SingleDependencyGraphWrapper from '@components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper'; import Button from '@components/button/Button'; -import formatNumber from '../../../utils/formatNumber'; -import CloseIcon from '../../icons/CloseIcon'; -import PlusIcon from '../../icons/PlusIcon'; -import Sidepanel from '../../sidepanel/Sidepanel'; -import SidepanelTabs from '../../sidepanel/SidepanelTabs'; +import CloseIcon from '@components/icons/CloseIcon'; +import PlusIcon from '@components/icons/PlusIcon'; +import Sidepanel from '@components/sidepanel/Sidepanel'; +import SidepanelTabs from '@components/sidepanel/SidepanelTabs'; +import formatNumber from '@utils/formatNumber'; import { InventoryItem, Pages, diff --git a/dashboard/services/settingsService.ts b/dashboard/services/settingsService.ts index 6d8a0a23c..4490dd034 100644 --- a/dashboard/services/settingsService.ts +++ b/dashboard/services/settingsService.ts @@ -95,16 +95,6 @@ const settingsService = { return Error; } }, - - async getResourceRelations(resourceId: string) { - try { - const res = await fetch(`${BASE_URL}/resources?resourceId=${resourceId}`); - const data = await res.json(); - return data; - } catch (error) { - return Error; - } - }, async getGlobalResources(payload: string) { try { const res = await fetch( diff --git a/dashboard/styles/globals.css b/dashboard/styles/globals.css index 655f53ae1..329404c37 100644 --- a/dashboard/styles/globals.css +++ b/dashboard/styles/globals.css @@ -55,14 +55,8 @@ } .popper-div { - 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, + 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; position: relative; color: #000; From 1f9d30f96a7ae9c9be12d0060ee082718a6a84a6 Mon Sep 17 00:00:00 2001 From: Saba Shavidze Date: Mon, 6 Nov 2023 12:36:59 +0400 Subject: [PATCH 4/9] fix: update related resource title --- .../single-resource-dependency-graph/SingleDependencyGraph.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx index 7a7af5197..4fcbf372e 100644 --- a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx +++ b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx @@ -78,7 +78,8 @@ const SingleDependencyGraph = ({ data }: DependencyGraphProps) => {
- {dataNodesLength} {`Resource${dataNodesLength > 1 ? 's' : ''}`} + {dataNodesLength}{' '} + {`related resource${dataNodesLength > 1 ? 's' : ''}`}
{/* Modal */} + { translateXClass, zoomVal, handleZoomChange - } = useDependencyGraphActions({ enableClickHandler: false }); + } = useDependencyGraphActions({ isSingleDependencyGraph: true }); return (
diff --git a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx index 7e606d855..871191c25 100644 --- a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx +++ b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx @@ -5,26 +5,35 @@ import HyperLinkIcon from '@components/icons/HyperLinkIcon'; import useDependencyGraph from '../hooks/useDependencyGraph'; import SingleDependencyGraphLoader from './SingleDependencyGraphLoader'; -function SingleDependencyGraphWrapper({ resourceId }: { resourceId?: string }) { +function SingleDependencyGraphWrapper({ + resourceId, + isInExplorer = false +}: { + resourceId: string; + isInExplorer?: boolean; +}) { const { loading, data, error, fetch } = useDependencyGraph(resourceId); const router = useRouter(); const title = 'Open in explorer'; + const encodedId = !isInExplorer && encodeURIComponent(resourceId); return ( <>
-
- + )} {!data?.nodes.length && !data?.edges.length ? (
) => ( ); diff --git a/dashboard/components/icons/AlertCircleIconFilled.tsx b/dashboard/components/icons/AlertCircleIconFilled.tsx index 5e9559412..1846dca2b 100644 --- a/dashboard/components/icons/AlertCircleIconFilled.tsx +++ b/dashboard/components/icons/AlertCircleIconFilled.tsx @@ -14,8 +14,8 @@ const AlertCircleIconFilled = (props: SVGProps) => ( diff --git a/dashboard/components/icons/DocumentTextIcon.tsx b/dashboard/components/icons/DocumentTextIcon.tsx index 15ee47823..5565dee7a 100644 --- a/dashboard/components/icons/DocumentTextIcon.tsx +++ b/dashboard/components/icons/DocumentTextIcon.tsx @@ -12,34 +12,34 @@ const DocumentTextIcon = (props: SVGProps) => ( ); diff --git a/dashboard/components/icons/DragIcon.tsx b/dashboard/components/icons/DragIcon.tsx index 6bd240499..539002f09 100644 --- a/dashboard/components/icons/DragIcon.tsx +++ b/dashboard/components/icons/DragIcon.tsx @@ -12,8 +12,8 @@ const DragIcon = (props: SVGProps) => ( ); diff --git a/dashboard/components/icons/Folder2Icon.tsx b/dashboard/components/icons/Folder2Icon.tsx index d307d2ac9..8624d9fdd 100644 --- a/dashboard/components/icons/Folder2Icon.tsx +++ b/dashboard/components/icons/Folder2Icon.tsx @@ -12,16 +12,16 @@ const Folder2Icon = (props: SVGProps) => ( ); diff --git a/dashboard/components/icons/KeyIcon.tsx b/dashboard/components/icons/KeyIcon.tsx index 53181b28a..c50bd6bb7 100644 --- a/dashboard/components/icons/KeyIcon.tsx +++ b/dashboard/components/icons/KeyIcon.tsx @@ -12,25 +12,25 @@ const KeyIcon = (props: SVGProps) => ( ); diff --git a/dashboard/components/icons/RecordCircleIcon.tsx b/dashboard/components/icons/RecordCircleIcon.tsx index a9de91820..8f87818c6 100644 --- a/dashboard/components/icons/RecordCircleIcon.tsx +++ b/dashboard/components/icons/RecordCircleIcon.tsx @@ -12,17 +12,17 @@ const RecordCircleIcon = (props: SVGProps) => ( ); diff --git a/dashboard/components/icons/ShieldSecurityIcon.tsx b/dashboard/components/icons/ShieldSecurityIcon.tsx index 7ece47d88..4011a380f 100644 --- a/dashboard/components/icons/ShieldSecurityIcon.tsx +++ b/dashboard/components/icons/ShieldSecurityIcon.tsx @@ -12,25 +12,25 @@ const ShieldSecurityIcon = (props: SVGProps) => ( ); diff --git a/dashboard/components/icons/VariableIcon.tsx b/dashboard/components/icons/VariableIcon.tsx index 327e09b93..9765b5199 100644 --- a/dashboard/components/icons/VariableIcon.tsx +++ b/dashboard/components/icons/VariableIcon.tsx @@ -16,9 +16,9 @@ const VariableIcon = (props: SVGProps) => ( ); diff --git a/dashboard/components/inventory/components/InventorySidePanel.tsx b/dashboard/components/inventory/components/InventorySidePanel.tsx index b6ae3a6f5..362ee8395 100644 --- a/dashboard/components/inventory/components/InventorySidePanel.tsx +++ b/dashboard/components/inventory/components/InventorySidePanel.tsx @@ -239,7 +239,7 @@ function InventorySidePanel({ {/* Relations */} {tabs.includes('relations') && ( - + )}
diff --git a/dashboard/components/inventory/hooks/useInventory/useInventory.tsx b/dashboard/components/inventory/hooks/useInventory/useInventory.tsx index ceb6fb4ba..6d11770a6 100644 --- a/dashboard/components/inventory/hooks/useInventory/useInventory.tsx +++ b/dashboard/components/inventory/hooks/useInventory/useInventory.tsx @@ -55,7 +55,9 @@ function useInventory() { const isVisible = useIsVisible(reloadDiv); const batchSize: number = 50; const router = useRouter(); - + useEffect(() => { + console.log('use inventory', { isOpen }); + }, [isOpen]); /** Reset most of the UI states: * - skipped (used to skip results in the data fetch call) * - skippedSearch (same, but used to skip results in the searched data fetch call) diff --git a/dashboard/pages/explorer.tsx b/dashboard/pages/explorer.tsx index 277f9c261..cfdad5f31 100644 --- a/dashboard/pages/explorer.tsx +++ b/dashboard/pages/explorer.tsx @@ -1,5 +1,5 @@ import Head from 'next/head'; -import DashboardDependencyGraphWrapper from '../components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphWrapper'; +import DashboardDependencyGraphWrapper from '@components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphWrapper'; function Explorer() { return ( diff --git a/dashboard/pages/inventory.tsx b/dashboard/pages/inventory.tsx index d4917be34..45a59722e 100644 --- a/dashboard/pages/inventory.tsx +++ b/dashboard/pages/inventory.tsx @@ -14,7 +14,6 @@ import SkeletonFilters from '../components/skeleton/SkeletonFilters'; import SkeletonInventory from '../components/skeleton/SkeletonInventory'; import SkeletonStats from '../components/skeleton/SkeletonStats'; import VerticalSpacing from '../components/spacing/VerticalSpacing'; -import Toast from '../components/toast/Toast'; export default function Inventory() { const { From 13412756b74022f369a508aa3204804154b34128 Mon Sep 17 00:00:00 2001 From: Saba Shavidze Date: Wed, 15 Nov 2023 14:13:07 +0400 Subject: [PATCH 6/9] fix: provide way to show single node relations dependency in explorer --- .../SingleDependencyGraphWrapper.tsx | 2 +- dashboard/pages/explorer.tsx | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx index 871191c25..75d53a945 100644 --- a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx +++ b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx @@ -24,7 +24,7 @@ function SingleDependencyGraphWrapper({
router.push(`/explorer/${encodedId}`)} + onClick={() => router.push(`/explorer?resourceId=${encodedId}`)} rel="noreferrer" className="hover:text-primary" > diff --git a/dashboard/pages/explorer.tsx b/dashboard/pages/explorer.tsx index cfdad5f31..f1aea813c 100644 --- a/dashboard/pages/explorer.tsx +++ b/dashboard/pages/explorer.tsx @@ -1,7 +1,11 @@ import Head from 'next/head'; import DashboardDependencyGraphWrapper from '@components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphWrapper'; +import { useRouter } from 'next/router'; +import SingleDependencyGraphWrapper from '@components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper'; function Explorer() { + const router = useRouter(); + const { resourceId } = router.query; return ( <> @@ -9,7 +13,14 @@ function Explorer() { - + {resourceId ? ( + + ) : ( + + )} ); } From c44ad12bf9fa31f8a01938e78ea9d0cc3fec198c Mon Sep 17 00:00:00 2001 From: Saba Shavidze Date: Wed, 15 Nov 2023 14:13:36 +0400 Subject: [PATCH 7/9] fix: provide way to show single node relations dependency in explorer --- dashboard/pages/explorer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/pages/explorer.tsx b/dashboard/pages/explorer.tsx index f1aea813c..6d7115275 100644 --- a/dashboard/pages/explorer.tsx +++ b/dashboard/pages/explorer.tsx @@ -1,7 +1,7 @@ import Head from 'next/head'; import DashboardDependencyGraphWrapper from '@components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraphWrapper'; import { useRouter } from 'next/router'; -import SingleDependencyGraphWrapper from '@components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper'; +import DashboardSingleDependencyGraphWrapper from '@components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper'; function Explorer() { const router = useRouter(); @@ -14,7 +14,7 @@ function Explorer() { {resourceId ? ( - From ff525031f5c70de541c05bdf295ac92ec7044d37 Mon Sep 17 00:00:00 2001 From: Saba Shavidze Date: Mon, 27 Nov 2023 19:33:50 +0400 Subject: [PATCH 8/9] fix: update the explorer page logic to handle resource id --- .../hooks/useDependencyGraphActions.tsx | 22 +++++++++++ .../DependencyGraph.tsx | 2 +- .../SingleDependencyGraphWrapper.tsx | 39 ++++++++----------- dashboard/pages/explorer.tsx | 13 +------ 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraphActions.tsx b/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraphActions.tsx index 5671dbcf6..abe2adf11 100644 --- a/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraphActions.tsx +++ b/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraphActions.tsx @@ -38,6 +38,9 @@ export const useDependencyGraphActions = ({ const [zoomLevel, setZoomLevel] = useState(minZoom); const [zoomVal, setZoomVal] = useState(0); // debounced zoom state to display percentage const cyRef = useRef(null); + const resourceId = JSON.parse(localStorage.getItem('resourceId') || ''); + + const [zoomToResourceId, setZoomToResourceId] = useState(false); // opens modal to display details of clicked node const handleNodeClick = async (event: EventObject) => { @@ -154,8 +157,27 @@ export const useDependencyGraphActions = ({ }); // Make sure to tell we inited successfully and prevent another init setInitDone(true); + + if (resourceId && cyRef.current) { + const targetNode = cyRef.current.getElementById(resourceId); + + if (targetNode.length > 0) { + cyRef.current.fit(targetNode); + setZoomToResourceId(true); + } + } } }; + useEffect(() => { + if (cyRef.current && zoomToResourceId) { + const targetNode = cyRef.current.getElementById(resourceId); + + if (targetNode.length > 0) { + cyRef.current.fit(targetNode); + setZoomToResourceId(false); // Reset the state after zooming + } + } + }, [zoomToResourceId, cyRef.current]); const handleZoomChange = (zoomPercentage: number) => { let newZoomLevel = minZoom + zoomPercentage * ((maxZoom - minZoom) / 100); diff --git a/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx index 1e9a03b2b..7539603de 100644 --- a/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx +++ b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx @@ -51,7 +51,7 @@ const DependencyGraph = ({ data }: DependencyGraphProps) => { translateXClass, zoomVal, handleZoomChange - } = useDependencyGraphActions({ isSingleDependencyGraph: false, openModal }); + } = useDependencyGraphActions({ openModal }); return (
diff --git a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx index 75d53a945..fb595f7da 100644 --- a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx +++ b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx @@ -5,35 +5,30 @@ import HyperLinkIcon from '@components/icons/HyperLinkIcon'; import useDependencyGraph from '../hooks/useDependencyGraph'; import SingleDependencyGraphLoader from './SingleDependencyGraphLoader'; -function SingleDependencyGraphWrapper({ - resourceId, - isInExplorer = false -}: { - resourceId: string; - isInExplorer?: boolean; -}) { +function SingleDependencyGraphWrapper({ resourceId }: { resourceId: string }) { const { loading, data, error, fetch } = useDependencyGraph(resourceId); const router = useRouter(); const title = 'Open in explorer'; - const encodedId = !isInExplorer && encodeURIComponent(resourceId); return ( <>
- {!isInExplorer && ( -
- + + {!data?.nodes.length && !data?.edges.length ? (
@@ -13,14 +9,7 @@ function Explorer() { - {resourceId ? ( - - ) : ( - - )} + ); } From 0155a48740db2b213cd59640bc399dc65a84babb Mon Sep 17 00:00:00 2001 From: Saba Shavidze Date: Mon, 27 Nov 2023 19:35:01 +0400 Subject: [PATCH 9/9] Merge branch 'feat-add-tabs-to-inventory-sidepanel' of https://github.com/shavidze/komiser into feat-add-tabs-to-inventory-sidepanel merge --- .../useInventory/helpers/getCustomViewInventoryListAndStats.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/components/inventory/hooks/useInventory/helpers/getCustomViewInventoryListAndStats.ts b/dashboard/components/inventory/hooks/useInventory/helpers/getCustomViewInventoryListAndStats.ts index add4ed428..d8a4827e2 100644 --- a/dashboard/components/inventory/hooks/useInventory/helpers/getCustomViewInventoryListAndStats.ts +++ b/dashboard/components/inventory/hooks/useInventory/helpers/getCustomViewInventoryListAndStats.ts @@ -110,7 +110,7 @@ function getCustomViewInventoryListAndStats({ } } }); - } + } // TODO: https://github.com/tailwarden/komiser/issues/1208 // else { // setTimeout(() => router.push(router.pathname), 5000);