diff --git a/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraph.tsx b/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraph.tsx index 54a078ff0fadf..2fb3406b217b2 100644 --- a/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraph.tsx +++ b/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraph.tsx @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ import React from 'react'; +import { useHistory, useParams } from 'react-router-dom'; import { Model, SELECTION_EVENT, @@ -10,11 +11,12 @@ import { TopologyControlBar, useVisualizationController, Visualization, - VisualizationProvider, VisualizationSurface, + VisualizationProvider, NodeModel, } from '@patternfly/react-topology'; +import { networkBasePathPF } from 'routePaths'; import stylesComponentFactory from './components/stylesComponentFactory'; import defaultLayoutFactory from './layouts/defaultLayoutFactory'; import defaultComponentFactory from './components/defaultComponentFactory'; @@ -49,44 +51,88 @@ function getUrlParamsForEntity(selectedEntity: NodeModel): [UrlDetailTypeValue, } export type NetworkGraphProps = { - detailType?: UrlDetailTypeValue; - detailId?: string; model: Model; - closeSidebar: () => void; - onSelectNode: (type, id) => void; }; -export type TopologyComponentProps = NetworkGraphProps; +export type TopologyComponentProps = { + model: Model; +}; + +function getNodeEdges(selectedNode) { + const egressEdges = selectedNode.getSourceEdges(); + const ingressEdges = selectedNode.getTargetEdges(); + return [...egressEdges, ...ingressEdges]; +} -const TopologyComponent = ({ - detailId, - model, - closeSidebar, - onSelectNode, -}: TopologyComponentProps) => { - const selectedEntity = findEntityById(model, detailId); +function setVisibleEdges(edges) { + edges.forEach((edge) => { + edge.setVisible(true); + }); +} + +function setEdges(controller, detailId) { + controller + .getGraph() + .getEdges() + .forEach((edge) => { + edge.setVisible(false); + }); + + if (detailId) { + const selectedNode = controller.getNodeById(detailId); + if (selectedNode?.isGroup()) { + selectedNode.getAllNodeChildren().forEach((child) => { + // set visible edges + setVisibleEdges(getNodeEdges(child)); + }); + } else if (selectedNode) { + // set visible edges + setVisibleEdges(getNodeEdges(selectedNode)); + } + } +} +const TopologyComponent = ({ model }: TopologyComponentProps) => { + const history = useHistory(); + const { detailId } = useParams(); + const selectedEntity = detailId && findEntityById(model, detailId); const controller = useVisualizationController(); - React.useEffect(() => { - function onSelect(ids: string[]) { - const newSelectedId = ids?.[0] || ''; - const newSelectedEntity = findEntityById(model, newSelectedId); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - if (newSelectedEntity) { - const [newDetailType, newDetailId] = getUrlParamsForEntity(newSelectedEntity); - onSelectNode(newDetailType, newDetailId); + // to prevent error where graph hasn't initialized yet + if (controller.hasGraph()) { + setEdges(controller, detailId); + } + + function closeSidebar() { + history.push(`${networkBasePathPF}`); + } + + function onSelect(ids: string[]) { + const newSelectedId = ids?.[0] || ''; + const newSelectedEntity = findEntityById(model, newSelectedId); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (newSelectedEntity) { + const [newDetailType, newDetailId] = getUrlParamsForEntity(newSelectedEntity); + // if found, and it's not the logical grouping of all external sources, then trigger URL update + if (newDetailId !== 'EXTERNAL') { + history.push(`${networkBasePathPF}/${newDetailType}/${newDetailId}`); + } else { + // otherwise, return to the graph-only state + history.push(`${networkBasePathPF}`); } } + } + React.useEffect(() => { controller.fromModel(model, false); controller.addEventListener(SELECTION_EVENT, onSelect); + setEdges(controller, detailId); return () => { controller.removeEventListener(SELECTION_EVENT, onSelect); }; - }, [controller, model, onSelectNode]); + }, [controller, model]); const selectedIds = selectedEntity ? [selectedEntity.id] : []; @@ -139,28 +185,20 @@ const TopologyComponent = ({ ); }; -const NetworkGraph = React.memo( - ({ detailType, detailId, closeSidebar, onSelectNode, model }) => { - const controller = new Visualization(); - controller.registerLayoutFactory(defaultLayoutFactory); - controller.registerComponentFactory(defaultComponentFactory); - controller.registerComponentFactory(stylesComponentFactory); - - return ( -
- - - -
- ); - } -); +const NetworkGraph = React.memo(({ model }) => { + const controller = new Visualization(); + controller.registerLayoutFactory(defaultLayoutFactory); + controller.registerComponentFactory(defaultComponentFactory); + controller.registerComponentFactory(stylesComponentFactory); + + return ( +
+ + + +
+ ); +}); NetworkGraph.displayName = 'NetworkGraph'; diff --git a/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraphPage.tsx b/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraphPage.tsx index 9d5c8d1ae170d..6a81bdb3e490d 100644 --- a/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraphPage.tsx +++ b/ui/apps/platform/src/Containers/NetworkGraph/NetworkGraphPage.tsx @@ -1,11 +1,9 @@ import React, { useEffect, useState } from 'react'; -import { useParams, useHistory } from 'react-router-dom'; -import { PageSection, Title, Flex, FlexItem } from '@patternfly/react-core'; +import { PageSection, Title, Flex, FlexItem, Bullseye, Spinner } from '@patternfly/react-core'; import { Model } from '@patternfly/react-topology'; import { fetchNetworkFlowGraph } from 'services/NetworkService'; import { fetchClustersAsArray, Cluster } from 'services/ClustersService'; -import { networkBasePathPF } from 'routePaths'; import PageTitle from 'Components/PageTitle'; import NetworkGraph from './NetworkGraph'; @@ -13,12 +11,13 @@ import { transformData, graphModel } from './utils'; import './NetworkGraphPage.css'; +const emptyModel = { + graph: graphModel, +}; + function NetworkGraphPage() { - const history = useHistory(); - const { detailType, detailId } = useParams(); - const [model, setModel] = useState({ - graph: graphModel, - }); + const [model, setModel] = useState(emptyModel); + const [isLoading, setIsLoading] = useState(false); const [clusters, setClusters] = useState([]); useEffect(() => { @@ -33,6 +32,7 @@ function NetworkGraphPage() { useEffect(() => { if (clusters.length > 0) { + setIsLoading(true); fetchNetworkFlowGraph(clusters[0].id, []) .then(({ response }) => { const dataModel = transformData(response.nodes); @@ -40,24 +40,11 @@ function NetworkGraphPage() { }) .catch(() => { // TODO - }); + }) + .finally(() => setIsLoading(false)); } }, [clusters]); - function onSelectNode(type: string, id: string) { - // if found, and it's not the logical grouping of all external sources, then trigger URL update - if (id !== 'EXTERNAL') { - history.push(`${networkBasePathPF}/${type}/${id}`); - } else { - // otherwise, return to the graph-only state - history.push(`${networkBasePathPF}`); - } - } - - function closeSidebar() { - history.push(`${networkBasePathPF}`); - } - return ( <> @@ -69,13 +56,12 @@ function NetworkGraphPage() { - + {model.nodes && } + {isLoading && ( + + + + )} ); diff --git a/ui/apps/platform/src/Containers/NetworkGraph/utils.tsx b/ui/apps/platform/src/Containers/NetworkGraph/utils.tsx index cf5ccb6a8f4c6..fa26d34cf7483 100644 --- a/ui/apps/platform/src/Containers/NetworkGraph/utils.tsx +++ b/ui/apps/platform/src/Containers/NetworkGraph/utils.tsx @@ -70,8 +70,7 @@ export function transformData(nodes: Node[]): Model { type: 'edge', source: entity.id, target: nodes[nodeIdx].entity.id, - // TODO: figure out how to conditionally render performantly - // visible: false, + visible: false, }; dataModel.edges.push(edge); });