diff --git a/dashboard/components/dashboard/components/cost-explorer/DashboardCostExplorerCard.tsx b/dashboard/components/dashboard/components/cost-explorer/DashboardCostExplorerCard.tsx index 83318b374..b7e4ce538 100644 --- a/dashboard/components/dashboard/components/cost-explorer/DashboardCostExplorerCard.tsx +++ b/dashboard/components/dashboard/components/cost-explorer/DashboardCostExplorerCard.tsx @@ -118,11 +118,11 @@ function DashboardCostExplorerCard({
{chartData && } {!chartData && ( -
-
+
+

No data for this time period

-

+

Our cloud version, Tailwarden, supports
historical costs from certain cloud providers

diff --git a/dashboard/components/explorer/DependencyGraph.tsx b/dashboard/components/explorer/DependencyGraph.tsx deleted file mode 100644 index 2bc87067e..000000000 --- a/dashboard/components/explorer/DependencyGraph.tsx +++ /dev/null @@ -1,347 +0,0 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import React, { useState, memo, useEffect, useRef } 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 EmptyState from '@components/empty-state/EmptyState'; - -import Tooltip from '@components/tooltip/Tooltip'; -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 { - edgeAnimationConfig, - edgeStyleConfig, - graphLayoutConfig, - leafStyleConfig, - maxZoom, - minZoom, - nodeHTMLLabelConfig, - nodeStyeConfig, - // popperStyleConfig, - zoomLevelBreakpoint -} from './config'; - -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 [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, - page, - goTo, - tags, - handleChange, - addNewTag, - removeTag, - updateTags, - loading, - deleteLoading, - bulkItems, - 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 ? ( - <> -
- -
- - ) : ( - <> - { - cyActionHandlers(cy); - cyRef.current = cy; - }} - /> - - )} -
-
-
- {data?.nodes?.length} Resources - {!dataIsEmpty && ( -
- - - Only AWS and CIVO resources are currently supported on the - explorer. - -
- )} -
-
- - - {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} - /> - - % - -
-
-
-
- {/* Modal */} - -
- ); -}; - -export default memo(DependencyGraph); 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 73% rename from dashboard/components/explorer/hooks/useDependencyGraph.tsx rename to dashboard/components/explorer/dependency-graph/hooks/useDependencyGraph.tsx index 1aa814142..b45bdfd3a 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'; @@ -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() { +function useDependencyGraph(resourceId?: string) { const [loading, setLoading] = useState(true); const [data, setData] = useState(); const [error, setError] = useState(false); @@ -75,8 +79,44 @@ function useDependencyGraph() { useState(); const router = useRouter(); + const fetchRelationsByResourceId = useCallback( + (id: string) => { + settingsService + .getResourceById(`?resourceId=${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]); - function fetch() { + const fetch = useCallback(() => { if (!loading) { setLoading(true); } @@ -84,17 +124,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(); + }, [filters, resourceId]); function deleteFilter(idx: number) { const updatedFilters: InventoryFilterData[] = [...filters!]; 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..abe2adf11 --- /dev/null +++ b/dashboard/components/explorer/dependency-graph/hooks/useDependencyGraphActions.tsx @@ -0,0 +1,235 @@ +// 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 { InventoryItem } from '@components/inventory/hooks/useInventory/types/useInventoryTypes'; +import { + edgeAnimationConfig, + maxZoom, + minZoom, + nodeHTMLLabelConfig, + zoomLevelBreakpoint +} from '../config'; + +type UseDependencyGraphActionsT = { + isSingleDependencyGraph?: boolean; + openModal?: (inventoryItem: InventoryItem) => void; +}; + +// Register the extensions only once +nodeHtmlLabel(Cytoscape.use(COSEBilkent)); +Cytoscape.use(popper); + +export const useDependencyGraphActions = ({ + isSingleDependencyGraph = false, + openModal +}: 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 resourceId = JSON.parse(localStorage.getItem('resourceId') || ''); + + const [zoomToResourceId, setZoomToResourceId] = useState(false); + + // 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) { + if (openModal) 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 (!isSingleDependencyGraph) { + 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); + + 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); + 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 new file mode 100644 index 000000000..7539603de --- /dev/null +++ b/dashboard/components/explorer/dependency-graph/multi-resource-dependency-graph/DependencyGraph.tsx @@ -0,0 +1,169 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { memo } from 'react'; +import CytoscapeComponent from 'react-cytoscapejs'; +import Cytoscape from 'cytoscape'; + +import EmptyState from '@components/empty-state/EmptyState'; + +import Tooltip from '@components/tooltip/Tooltip'; +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 InventorySidePanel from '@components/inventory/components/InventorySidePanel'; +import { DependencyGraphProps } from '../hooks/useDependencyGraph'; +import { + edgeStyleConfig, + graphLayoutConfig, + leafStyleConfig, + maxZoom, + minZoom, + nodeStyeConfig +} from '../config'; +import { useDependencyGraphActions } from '../hooks/useDependencyGraphActions'; + +const DependencyGraph = ({ data }: DependencyGraphProps) => { + const dataIsEmpty: boolean = data.nodes.length === 0; + + const { + openModal, + isOpen, + closeModal, + data: inventoryItem, + page, + goTo, + tags, + handleChange, + addNewTag, + removeTag, + updateTags, + loading, + deleteLoading, + bulkItems, + updateBulkTags + } = useInventory(); + + const { + cyRef, + cyActionHandlers, + toggleNodeDragging, + isNodeDraggingEnabled, + translateXClass, + zoomVal, + handleZoomChange + } = useDependencyGraphActions({ openModal }); + + return ( +
+ {dataIsEmpty ? ( + <> +
+ +
+ + ) : ( + <> + { + cyActionHandlers(cy); + cyRef.current = cy; + }} + /> + + )} +
+
+
+ {data?.nodes?.length} Resources + {!dataIsEmpty && ( +
+ + + Only AWS resources are currently supported on the explorer. + +
+ )} +
+
+ + + {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} + /> + + % + +
+
+
+
+ {/* Modal */} + + +
+ ); +}; + +export default memo(DependencyGraph); 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 c20fe3e39..fa38df236 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-dependency-graph/SingleDependencyGraph.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx new file mode 100644 index 000000000..d113080cb --- /dev/null +++ b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraph.tsx @@ -0,0 +1,121 @@ +import React, { memo } from 'react'; +import CytoscapeComponent from 'react-cytoscapejs'; +import Cytoscape from 'cytoscape'; + +import EmptyState from '@components/empty-state/EmptyState'; + +import Tooltip from '@components/tooltip/Tooltip'; +import { DragIcon } from '@components/icons'; +import NumberInput from '@components/number-input/NumberInput'; +import { DependencyGraphProps } from '../hooks/useDependencyGraph'; +import { + edgeStyleConfig, + graphLayoutConfig, + leafStyleConfig, + maxZoom, + minZoom, + nodeStyeConfig +} from '../config'; +import { useDependencyGraphActions } from '../hooks/useDependencyGraphActions'; + +const SingleDependencyGraph = ({ data }: DependencyGraphProps) => { + const dataNodesLength: number = data.nodes.length; + const { + cyRef, + cyActionHandlers, + toggleNodeDragging, + isNodeDraggingEnabled, + translateXClass, + zoomVal, + handleZoomChange + } = useDependencyGraphActions({ isSingleDependencyGraph: true }); + + return ( +
+ {dataNodesLength === 0 ? ( + <> +
+ +
+ + ) : ( + <> + { + cyActionHandlers(cy); + cyRef.current = cy; + }} + /> + + )} +
+
+
+ {dataNodesLength}{' '} + {`related 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} + /> + + % + +
+
+
+
+
+ ); +}; + +export default memo(SingleDependencyGraph); diff --git a/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphLoader.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphLoader.tsx new file mode 100644 index 000000000..d189df6e4 --- /dev/null +++ b/dashboard/components/explorer/dependency-graph/single-resource-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-dependency-graph/SingleDependencyGraphWrapper.tsx b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx new file mode 100644 index 000000000..fb595f7da --- /dev/null +++ b/dashboard/components/explorer/dependency-graph/single-resource-dependency-graph/SingleDependencyGraphWrapper.tsx @@ -0,0 +1,63 @@ +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/icons/AlertCircleIcon.tsx b/dashboard/components/icons/AlertCircleIcon.tsx index 5763df243..81c5988f4 100644 --- a/dashboard/components/icons/AlertCircleIcon.tsx +++ b/dashboard/components/icons/AlertCircleIcon.tsx @@ -11,23 +11,23 @@ const AlertCircleIcon = (props: SVGProps) => ( ); 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 4f845aeb1..44b8df022 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-dependency-graph/SingleDependencyGraphWrapper'; import Button from '@components/button/Button'; import CloseIcon from '@components/icons/CloseIcon'; import PlusIcon from '@components/icons/PlusIcon'; @@ -107,10 +108,10 @@ function InventorySidePanel({
-

+

Cloud account

-

+

{!data && (

)} @@ -118,10 +119,10 @@ function InventorySidePanel({

-

+

Region

-

+

{!data && (

)} @@ -129,7 +130,7 @@ function InventorySidePanel({

-

+

Cost

@@ -145,10 +146,10 @@ function InventorySidePanel({

-

+

Relations

-

+

{!data && (

)} @@ -160,6 +161,7 @@ function InventorySidePanel({

)} + {/* Tags form */} {tabs.includes('tags') && ( @@ -233,6 +235,13 @@ function InventorySidePanel({ )} + + {/* Relations */} + {tabs.includes('relations') && ( + + + + )}
{page === 'delete' && ( <> 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); 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/components/onboarding-wizard/OnboardingWizardLayout.tsx b/dashboard/components/onboarding-wizard/OnboardingWizardLayout.tsx index 8c949264d..0c230817c 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 eea94dcd3..6b713d2c5 100644 --- a/dashboard/components/sidepanel/SidepanelHeader.tsx +++ b/dashboard/components/sidepanel/SidepanelHeader.tsx @@ -38,7 +38,7 @@ function SidepanelHeader({
{cloudProvider && }
-

+

{title}

-

- {subtitle} -

+

{subtitle}

)} @@ -62,7 +60,7 @@ function SidepanelHeader({
-

+

{title}

diff --git a/dashboard/components/sidepanel/SidepanelPage.tsx b/dashboard/components/sidepanel/SidepanelPage.tsx index 3fe551318..1656d937b 100644 --- a/dashboard/components/sidepanel/SidepanelPage.tsx +++ b/dashboard/components/sidepanel/SidepanelPage.tsx @@ -21,7 +21,7 @@ function SidepanelPage({ container ? 'rounded-lg bg-gray-50 p-6' : ' h-full overflow-auto' } > -
{children}
+
{children}
)} diff --git a/dashboard/pages/cloud-accounts.tsx b/dashboard/pages/cloud-accounts.tsx index 7164ccdb9..82450bc0e 100644 --- a/dashboard/pages/cloud-accounts.tsx +++ b/dashboard/pages/cloud-accounts.tsx @@ -91,7 +91,7 @@ function CloudAccounts() { {/* Wraps the cloud account page and handles the custom views sidebar */} -
+
{filteredCloudAccounts.map(account => ( = 2 && !isTailwardenBannerDismissed && ( -
+
For deeper insights and account-level alerts, make the switch to Tailwarden — our recommended cloud version for production use.{' '}