diff --git a/cypress/e2e/skupper-console/sites/site.cy.ts b/cypress/e2e/skupper-console/sites/site.cy.ts index 81b1894f0..fcb23e661 100644 --- a/cypress/e2e/skupper-console/sites/site.cy.ts +++ b/cypress/e2e/skupper-console/sites/site.cy.ts @@ -29,13 +29,4 @@ context('Sites', () => { link.click(); cy.url().should('eq', expectedUrl); }); - - it('should navigate to the correct process page when clicking on a link', () => { - const expectedUrl = 'http://localhost:3000/#/sites'; - - const link = cy.get(`[data-testid="${getTestsIds.breadcrumbComponent()}"]`).contains('sites'); - link.should('have.attr', 'href', `#/sites`); - link.click(); - cy.url().should('eq', expectedUrl); - }); }); diff --git a/mocks/server.ts b/mocks/server.ts index 1669348a0..b1e25d52f 100644 --- a/mocks/server.ts +++ b/mocks/server.ts @@ -15,7 +15,8 @@ import { ProcessGroupPairsResponse } from 'API/REST.interfaces'; -const DELAY_RESPONSE = 0; +const DELAY_RESPONSE = Number(process.env.MOCK_DELAY_RESPONSE) || 0; // in ms +const ITEM_COUNT = Number(process.env.MOCK_ITEM_COUNT) || 0; // api prefix const prefix = '/api/v1alpha1'; @@ -36,15 +37,13 @@ const serviceFlowPairs = require(`${path}/SERVICE_FLOW_PAIRS.json`); const routers: ResponseWrapper = require(`${path}/ROUTERS.json`); const links: ResponseWrapper = require(`${path}/LINKS.json`); -const PERF_TEST = false; -const ITEMS_TEST = 200; interface ApiProps { params: Record; - queryParams: Record; + queryParams: Record; } const mockSitesForPerf: SiteResponse[] = []; -for (let i = 0; i < ITEMS_TEST; i++) { +for (let i = 0; i < ITEM_COUNT; i++) { mockSitesForPerf.push({ recType: 'SITE', identity: `sitePerf${i}`, @@ -76,8 +75,8 @@ mockSitesForPerf.forEach((site, index) => { const mockLinksForPerf: LinkResponse[] = []; mockRoutersForPerf.forEach((_, index) => { - const idx1 = Math.floor(Math.random() * ITEMS_TEST); - const idx2 = Math.floor(Math.random() * ITEMS_TEST); + const idx1 = Math.floor(Math.random() * ITEM_COUNT); + const idx2 = Math.floor(Math.random() * ITEM_COUNT); const router1 = mockRoutersForPerf[idx1]; const router2 = mockRoutersForPerf[idx2]; @@ -109,7 +108,7 @@ mockRoutersForPerf.forEach((_, index) => { }); const mockProcessesForPerf: ProcessResponse[] = []; -for (let i = 0; i < ITEMS_TEST; i++) { +for (let i = 0; i < ITEM_COUNT; i++) { // const parent = Math.floor(Math.random() * (4 - 1 + 1) + 1); const process = processes.results[i % processes.results.length]; @@ -125,7 +124,7 @@ for (let i = 0; i < ITEMS_TEST; i++) { } const mockProcessPairsForPerf: ProcessPairsResponse[] = []; -for (let i = 0; i < ITEMS_TEST; i++) { +for (let i = 0; i < ITEM_COUNT; i++) { const sourceIndex = Math.floor(Math.random() * mockProcessesForPerf.length); const destinationIndex = Math.floor(Math.random() * mockProcessesForPerf.length); @@ -146,7 +145,7 @@ export const MockApi = { get404Error: () => new Response(404), getCollectors: () => collectors, getSites: () => { - const sitesForPerfTests = PERF_TEST ? mockSitesForPerf : []; + const sitesForPerfTests = ITEM_COUNT ? mockSitesForPerf : []; const results = [...sites.results, ...sitesForPerfTests]; return { ...sites, results }; @@ -155,13 +154,13 @@ export const MockApi = { results: sites.results.find(({ identity }) => identity === id) || [] }), getRouters: () => { - const routersForPerfTests = PERF_TEST ? mockRoutersForPerf : []; + const routersForPerfTests = ITEM_COUNT ? mockRoutersForPerf : []; const results = [...routers.results, ...routersForPerfTests]; return { ...routers, results }; }, getLinks: () => { - const linksForPerfTests = PERF_TEST ? mockLinksForPerf : []; + const linksForPerfTests = ITEM_COUNT ? mockLinksForPerf : []; const results = [...links.results, ...linksForPerfTests]; return { ...links, results }; @@ -173,25 +172,42 @@ export const MockApi = { return { results }; }, getProcesses: (_: unknown, { queryParams }: ApiProps) => { - const processesForPerfTests = PERF_TEST ? mockProcessesForPerf : []; + const processesForPerfTests = ITEM_COUNT ? mockProcessesForPerf : []; const results = [...processes.results, ...processesForPerfTests]; if (queryParams && !Object.keys(queryParams).length) { - return { ...processes, results }; + return { + ...processes, + results, + count: results.length, + totalCount: results.length, + timeRangeCount: results.length + }; } - const resultFIltered = results.filter( + const filteredResults = results.filter( (result) => result.processRole === queryParams.processRole || result.groupIdentity === queryParams.groupIdentity || result.parent === queryParams.parent ); - return { ...processes, results: resultFIltered }; + const paginatedResults = filteredResults.slice( + Number(queryParams.offset || 0), + Number(queryParams.offset || 0) + Number(queryParams.limit || filteredResults.length - 1) + ); + + return { + ...processes, + results: paginatedResults, + count: filteredResults.length, + totalCount: filteredResults.length, + timeRangeCount: filteredResults.length + }; }, getProcessPairs: (_: unknown, { queryParams }: ApiProps) => { - const processesForPerfTests = PERF_TEST ? mockProcessPairsForPerf : []; + const processesForPerfTests = ITEM_COUNT ? mockProcessPairsForPerf : []; const results = [...processPairs.results, ...processesForPerfTests]; if (queryParams && !Object.keys(queryParams).length) { @@ -207,7 +223,7 @@ export const MockApi = { }, getPrometheusQuery: (_: unknown, { queryParams }: ApiProps) => { - if (queryParams.query === 'sum by(destProcess, sourceProcess,direction)(rate(octets_total[1m]))') { + if ((queryParams.query as string)?.includes('sum by(destProcess, sourceProcess, direction)(rate(octets_total')) { return { data: { resultType: 'vector', @@ -330,7 +346,7 @@ export function loadMockServer() { })); this.get(`${prefix}/processes/:id`, (_, { params: { id } }) => ({ - results: (PERF_TEST ? mockProcessesForPerf : processes.results).find( + results: MockApi.getProcesses(null, { params: {}, queryParams: {} }).results.find( ({ identity }: ProcessResponse) => identity === id ) })); diff --git a/src/API/Prometheus.api.ts b/src/API/Prometheus.api.ts index d0852b1ab..9797e69e9 100644 --- a/src/API/Prometheus.api.ts +++ b/src/API/Prometheus.api.ts @@ -125,31 +125,46 @@ export const PrometheusApi = { return result; }, - fetchAllProcessPairsBytes: async (groupBy: string): Promise => { + fetchAllProcessPairsBytes: async ( + groupBy: string, + filters?: PrometheusLabels + ): Promise => { + const queryFilterString = filters ? convertToPrometheusQueryParams(filters) : undefined; + const { data: { result } } = await axiosFetch>(gePrometheusQueryPATH('single'), { - params: { query: queries.getAllPairsBytes(groupBy) } + params: { query: queries.getAllPairsBytes(groupBy, queryFilterString) } }); return result; }, - fetchAllProcessPairsByteRates: async (groupBy: string): Promise => { + fetchAllProcessPairsByteRates: async ( + groupBy: string, + filters?: PrometheusLabels + ): Promise => { + const queryFilterString = filters ? convertToPrometheusQueryParams(filters) : undefined; + const { data: { result } } = await axiosFetch>(gePrometheusQueryPATH('single'), { - params: { query: queries.getAllPairsByteRates(groupBy) } + params: { query: queries.getAllPairsByteRates(groupBy, queryFilterString) } }); return result; }, - fetchAllProcessPairsLatencies: async (groupBy: string): Promise => { + fetchAllProcessPairsLatencies: async ( + groupBy: string, + filters?: PrometheusLabels + ): Promise => { + const queryFilterString = filters ? convertToPrometheusQueryParams(filters) : undefined; + const { data: { result } } = await axiosFetch>(gePrometheusQueryPATH('single'), { - params: { query: queries.getAllPairsLatencies(groupBy) } + params: { query: queries.getAllPairsLatencies(groupBy, queryFilterString) } }); return result; diff --git a/src/API/Prometheus.queries.ts b/src/API/Prometheus.queries.ts index 9b739b0f8..1bb9270a8 100644 --- a/src/API/Prometheus.queries.ts +++ b/src/API/Prometheus.queries.ts @@ -31,15 +31,27 @@ export const queries = { }, // topology metrics - getAllPairsBytes(groupBy: string) { + getAllPairsBytes(groupBy: string, params?: string) { + if (params) { + return `sum by(${groupBy})(octets_total{${params}})`; + } + return `sum by(${groupBy})(octets_total)`; }, - getAllPairsByteRates(groupBy: string) { + getAllPairsByteRates(groupBy: string, params?: string) { + if (params) { + return `sum by(${groupBy})(rate(octets_total{${params}}[1m]))`; + } + return `sum by(${groupBy})(rate(octets_total[1m]))`; }, - getAllPairsLatencies(groupBy: string) { + getAllPairsLatencies(groupBy: string, params?: string) { + if (params) { + return `sum by(${groupBy})(rate(flow_latency_microseconds_sum{${params}}[1m]))`; + } + return `sum by(${groupBy})(rate(flow_latency_microseconds_sum[1m]))`; }, diff --git a/src/core/components/Graph/ReactAdaptor.tsx b/src/core/components/Graph/ReactAdaptor.tsx index e9aa9d47f..aa7a45ee6 100644 --- a/src/core/components/Graph/ReactAdaptor.tsx +++ b/src/core/components/Graph/ReactAdaptor.tsx @@ -80,14 +80,14 @@ const GraphReactAdaptor: FC = memo( if (!graphInstance) { return; } - const nodeFound = graphInstance.find('node', (node) => node.getModel().id === id); - if (nodeFound) { - graphInstance.focusItem(nodeFound, true, { duration: 100 }); + if (id) { handleMouseEnter(id); + graphInstance.focusItem(id, true, { duration: 100 }); return; } + handleNodeMouseLeave({ currentTarget: graphInstance }); } })); @@ -184,6 +184,8 @@ const GraphReactAdaptor: FC = memo( /** Simulate a MouseEnter event, regardless of whether a node or edge is preselected */ const handleMouseEnter = useCallback( (id?: string) => { + isHoverState.current = true; + const graphInstance = topologyGraphRef.current; if (graphInstance && id) { @@ -337,6 +339,11 @@ const GraphReactAdaptor: FC = memo( const handleAfterRender = useCallback(() => { handleMouseEnter(itemSelectedRef.current); + + if (itemSelectedRef.current) { + topologyGraphRef.current?.focusItem(itemSelectedRef.current); + } + setIsGraphLoaded(true); }, [handleMouseEnter]); diff --git a/src/core/components/HighlightValueCell/index.tsx b/src/core/components/HighlightValueCell/index.tsx index 0be8bd155..7bea39023 100644 --- a/src/core/components/HighlightValueCell/index.tsx +++ b/src/core/components/HighlightValueCell/index.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react'; +import { useMemo, useRef } from 'react'; import { VarColors } from '@config/colors'; @@ -7,23 +7,23 @@ import { HighlightValueCellProps } from './HighightValueCell.interfaces'; const HighlightValueCell = function ({ value, format }: HighlightValueCellProps) { const prevValueRef = useRef(); - const isValueUpdated = useCallback(() => { + const isValueUpdated = useMemo(() => { if (!prevValueRef.current) { prevValueRef.current = value; return false; } - if (value !== prevValueRef.current) { + if (format(value) !== format(prevValueRef.current)) { prevValueRef.current = value; return true; } return false; - }, [value]); + }, [format, value]); - return isValueUpdated() ? ( + return isValueUpdated ? (
getIdAndNameFromUrlParams(path.replace(/%20/g, ' '))); //sanitize %20 url space @@ -15,11 +16,13 @@ const SkBreadcrumb = function () { return null; } + const queryParams = searchParams.size > 0 ? `?${searchParams.toString()}` : ''; + return ( {pathsNormalized.map((path, index) => ( - {path.name} + {path.name} ))} diff --git a/src/pages/Processes/Processes.constants.ts b/src/pages/Processes/Processes.constants.ts index 97890920f..b626c6446 100644 --- a/src/pages/Processes/Processes.constants.ts +++ b/src/pages/Processes/Processes.constants.ts @@ -3,7 +3,7 @@ import { HighlightValueCellProps } from '@core/components/HighlightValueCell/Hig import LinkCell from '@core/components/LinkCell'; import { LinkCellProps } from '@core/components/LinkCell/LinkCell.interfaces'; import { SKColumn } from '@core/components/SkTable/SkTable.interfaces'; -import { formatBytes } from '@core/utils/formatBytes'; +import { formatByteRate, formatBytes } from '@core/utils/formatBytes'; import { formatLatency } from '@core/utils/formatLatency'; import { timeAgo } from '@core/utils/timeAgo'; import { ProcessGroupsRoutesPaths } from '@pages/ProcessGroups/ProcessGroups.enum'; @@ -24,8 +24,14 @@ export const CustomProcessPairCells = { LinkCell({ ...props, type: 'process', - link: `${ProcessesRoutesPaths.Processes}/${props.data.sourceName}@${props.data.sourceId}/${ProcessesLabels.Title}@${props.data.identity}@${props.data.protocol}` - }) + link: `${ProcessesRoutesPaths.Processes}/${props.data.destinationName}@${props.data.destinationId}?type=${ProcessesLabels.ProcessPairs}` + }), + ByteFormatCell: (props: HighlightValueCellProps) => + HighlightValueCell({ ...props, format: formatBytes }), + ByteRateFormatCell: (props: HighlightValueCellProps) => + HighlightValueCell({ ...props, format: formatByteRate }), + LatencyFormatCell: (props: HighlightValueCellProps) => + HighlightValueCell({ ...props, format: formatLatency }) }; export const CustomProcessCells = { @@ -47,10 +53,10 @@ export const CustomProcessCells = { type: 'component', link: `${ProcessGroupsRoutesPaths.ProcessGroups}/${props.data.groupName}@${props.data.groupIdentity}` }), - ClientServerLatencyCell: (props: LinkCellProps) => - formatLatency(props.data.counterFlow.latency + props.data.forwardFlow.latency), ByteFormatCell: (props: HighlightValueCellProps) => - HighlightValueCell({ ...props, format: formatBytes }) + HighlightValueCell({ ...props, format: formatBytes }), + ByteRateFormatCell: (props: HighlightValueCellProps) => + HighlightValueCell({ ...props, format: formatByteRate }) }; export const processesTableColumns: SKColumn[] = [ @@ -82,6 +88,29 @@ export const processesConnectedColumns: SKColumn[] = [ name: ProcessesLabels.Process, prop: 'destinationName' as keyof ProcessPairsResponse, customCellName: 'ProcessConnectedLinkCell' + }, + { + name: ProcessesLabels.Bytes, + prop: 'bytes' as keyof ProcessPairsResponse, + customCellName: 'ByteFormatCell', + modifier: 'fitContent' + }, + { + name: ProcessesLabels.ByteRate, + prop: 'byteRate' as keyof ProcessPairsResponse, + customCellName: 'ByteRateFormatCell', + modifier: 'fitContent' + }, + { + name: ProcessesLabels.Latency, + prop: 'latency' as keyof ProcessPairsResponse, + customCellName: 'LatencyFormatCell', + modifier: 'fitContent' + }, + { + name: '', + customCellName: 'viewDetailsLinkCell', + modifier: 'fitContent' } ]; @@ -95,6 +124,29 @@ export const processesHttpConnectedColumns: SKColumn[] = [ name: ProcessesLabels.Protocol, prop: 'protocol' as keyof ProcessPairsResponse, modifier: 'fitContent' + }, + { + name: ProcessesLabels.Bytes, + prop: 'bytes' as keyof ProcessPairsResponse, + customCellName: 'ByteFormatCell', + modifier: 'fitContent' + }, + { + name: ProcessesLabels.ByteRate, + prop: 'byteRate' as keyof ProcessPairsResponse, + customCellName: 'ByteRateFormatCell', + modifier: 'fitContent' + }, + { + name: ProcessesLabels.Latency, + prop: 'latency' as keyof ProcessPairsResponse, + customCellName: 'LatencyFormatCell', + modifier: 'fitContent' + }, + { + name: '', + customCellName: 'viewDetailsLinkCell', + modifier: 'fitContent' } ]; diff --git a/src/pages/Processes/Processes.enum.ts b/src/pages/Processes/Processes.enum.ts index 3bf8300b8..926261b59 100644 --- a/src/pages/Processes/Processes.enum.ts +++ b/src/pages/Processes/Processes.enum.ts @@ -56,6 +56,9 @@ export enum ProcessesLabels { GoToDetails = 'View details', Title = 'Process pair details', Protocol = 'Protocol', + Bytes = 'Bytes', + ByteRate = 'Byterate', + Latency = 'Latency', Name = 'Name', Component = 'Component', ByteRateRx = 'Rx byte rate', diff --git a/src/pages/Processes/Processes.interfaces.ts b/src/pages/Processes/Processes.interfaces.ts new file mode 100644 index 000000000..54264bcd9 --- /dev/null +++ b/src/pages/Processes/Processes.interfaces.ts @@ -0,0 +1,19 @@ +import { ProcessResponse } from '@API/REST.interfaces'; + +export interface ProcessPairsProps { + process: ProcessResponse; +} + +export interface DetailsProps { + process: ProcessResponse; + title?: string | JSX.Element; +} + +export interface OverviewProps { + process: ProcessResponse; +} + +export interface ProcessPairProcessesProps { + sourceId: string; + destinationId: string; +} diff --git a/src/pages/Processes/__tests__/ProcessesPairs.spec.tsx b/src/pages/Processes/__tests__/ProcessesPairs.spec.tsx index 46b7b2ddb..2f9e76274 100644 --- a/src/pages/Processes/__tests__/ProcessesPairs.spec.tsx +++ b/src/pages/Processes/__tests__/ProcessesPairs.spec.tsx @@ -46,9 +46,9 @@ describe('Process component', () => { timeout: waitForElementToBeRemovedTimeout }); - expect(screen.getByRole('link', { name: processesData.results[8].name })).toHaveAttribute( + expect(screen.getAllByRole('link', { name: 'view pairs' })[0]).toHaveAttribute( 'href', - `#${ProcessesRoutesPaths.Processes}/${processPairsResult.destinationName}@${processPairsResult.destinationId}/${ProcessesLabels.Title}@${processPairsResult.identity}@${processPairsResult.protocol}` + `#${ProcessesRoutesPaths.Processes}/${processPairsResult.destinationName}@${processPairsResult.destinationId}/${ProcessesLabels.ProcessPairs}@${processesPairsData.results[6].identity}@${processesPairsData.results[6].protocol}?type=${ProcessesLabels.ProcessPairs}` ); }); }); diff --git a/src/pages/Processes/components/Details.tsx b/src/pages/Processes/components/Details.tsx index 0666a13a3..45aa9c1f5 100644 --- a/src/pages/Processes/components/Details.tsx +++ b/src/pages/Processes/components/Details.tsx @@ -25,14 +25,11 @@ import { timeAgo } from '@core/utils/timeAgo'; import { ProcessGroupsRoutesPaths } from '@pages/ProcessGroups/ProcessGroups.enum'; import { ServicesRoutesPaths } from '@pages/Services/Services.enum'; import { SitesRoutesPaths } from '@pages/Sites/Sites.enum'; -import { ProcessResponse } from 'API/REST.interfaces'; import { ProcessesLabels, QueriesProcesses } from '../Processes.enum'; +import { DetailsProps } from '../Processes.interfaces'; -const Details: FC<{ process: ProcessResponse; title?: string | JSX.Element }> = function ({ - process, - title = ProcessesLabels.Details -}) { +const Details: FC = function ({ process, title = ProcessesLabels.Details }) { const { identity: processId, parent, diff --git a/src/pages/Processes/components/Overview.tsx b/src/pages/Processes/components/Overview.tsx index 2da713057..593e6a91c 100644 --- a/src/pages/Processes/components/Overview.tsx +++ b/src/pages/Processes/components/Overview.tsx @@ -4,7 +4,6 @@ import { useQuery } from '@tanstack/react-query'; import { RESTApi } from '@API/REST.api'; import { AvailableProtocols } from '@API/REST.enum'; -import { ProcessResponse } from '@API/REST.interfaces'; import { UPDATE_INTERVAL } from '@config/config'; import { siteNameAndIdSeparator } from '@config/prometheus'; import { getDataFromSession, storeDataToSession } from '@core/utils/persistData'; @@ -13,14 +12,11 @@ import Metrics from '@pages/shared/Metrics'; import { ExpandedMetricSections, SelectedMetricFilters } from '@pages/shared/Metrics/Metrics.interfaces'; import { QueriesProcesses } from '../Processes.enum'; +import { OverviewProps } from '../Processes.interfaces'; const PREFIX_METRIC_FILTERS_CACHE_KEY = 'process-metric-filters'; const PREFIX_METRIC_OPEN_SECTION_CACHE_KEY = `process-open-metric-sections`; -interface OverviewProps { - process: ProcessResponse; -} - const Overview: FC = function ({ process: { identity: processId, name, startTime, parent, parentName } }) { diff --git a/src/pages/Processes/components/ProcessPairDetails.tsx b/src/pages/Processes/components/ProcessPairDetails.tsx new file mode 100644 index 000000000..2c651bb99 --- /dev/null +++ b/src/pages/Processes/components/ProcessPairDetails.tsx @@ -0,0 +1,66 @@ +import { FC } from 'react'; + +import { Bullseye, Grid, GridItem, Icon } from '@patternfly/react-core'; +import { LongArrowAltRightIcon } from '@patternfly/react-icons'; +import { useQuery } from '@tanstack/react-query'; + +import { RESTApi } from '@API/REST.api'; +import { ProcessResponse } from '@API/REST.interfaces'; +import { VarColors } from '@config/colors'; +import LinkCell from '@core/components/LinkCell'; + +import Details from './Details'; +import { ProcessesRoutesPaths, QueriesProcesses } from '../Processes.enum'; +import { ProcessPairProcessesProps } from '../Processes.interfaces'; + +const ProcessPairDetails: FC = function ({ sourceId, destinationId }) { + const { data: source } = useQuery({ + queryKey: [QueriesProcesses.GetProcess, sourceId], + queryFn: () => RESTApi.fetchProcess(sourceId) + }); + + const { data: destination } = useQuery({ + queryKey: [QueriesProcesses.GetDestination, destinationId], + queryFn: () => RESTApi.fetchProcess(destinationId) + }); + + if (!source || !destination) { + return null; + } + + return ( + + +
({ + data: source, + value: source.name, + link: `${ProcessesRoutesPaths.Processes}/${source.name}@${sourceId}` + })} + /> + + + + + + + + + + + +
({ + data: destination, + value: destination.name, + link: `${ProcessesRoutesPaths.Processes}/${destination.name}@${destinationId}` + })} + /> + + + ); +}; + +export default ProcessPairDetails; diff --git a/src/pages/Processes/components/ProcessesPairs.tsx b/src/pages/Processes/components/ProcessesPairs.tsx index f1a806126..cd587a205 100644 --- a/src/pages/Processes/components/ProcessesPairs.tsx +++ b/src/pages/Processes/components/ProcessesPairs.tsx @@ -2,25 +2,32 @@ import { FC } from 'react'; import { Flex } from '@patternfly/react-core'; import { useQuery } from '@tanstack/react-query'; +import { Link } from 'react-router-dom'; import { RESTApi } from '@API/REST.api'; import { AvailableProtocols } from '@API/REST.enum'; -import { ProcessResponse } from '@API/REST.interfaces'; +import { ProcessPairsResponse } from '@API/REST.interfaces'; import { SMALL_PAGINATION_SIZE, UPDATE_INTERVAL } from '@config/config'; +import { LinkCellProps } from '@core/components/LinkCell/LinkCell.interfaces'; import SkTable from '@core/components/SkTable'; +import { TopologyController } from '@pages/Topology/services'; +import { QueriesTopology } from '@pages/Topology/Topology.enum'; import { CustomProcessPairCells, processesConnectedColumns, processesHttpConnectedColumns } from '../Processes.constants'; -import { ProcessesLabels, QueriesProcesses } from '../Processes.enum'; +import { ProcessesLabels, ProcessesRoutesPaths, QueriesProcesses } from '../Processes.enum'; +import { ProcessPairsProps } from '../Processes.interfaces'; -interface ProcessPairsProps { - process: ProcessResponse; -} +const metricQueryParams = { + fetchBytes: { groupBy: 'destProcess, sourceProcess, direction' }, + fetchByteRate: { groupBy: 'destProcess, sourceProcess, direction' }, + fetchLatency: { groupBy: 'sourceProcess, destProcess' } +}; -const ProcessPairs: FC = function ({ process: { identity: processId } }) { +const ProcessPairs: FC = function ({ process: { identity: processId, name: processName } }) { const processesPairsTxQueryParams = { sourceId: processId }; @@ -41,8 +48,52 @@ const ProcessPairs: FC = function ({ process: { identity: pro refetchInterval: UPDATE_INTERVAL }); + const { data: metricsTx } = useQuery({ + queryKey: [QueriesTopology.GetBytesByProcessPairs, { sourceProcess: processName }], + queryFn: () => + TopologyController.getMetrics({ + showBytes: true, + showByteRate: true, + showLatency: true, + params: { + ...metricQueryParams, + filterBy: { sourceProcess: processName } + } + }), + refetchInterval: UPDATE_INTERVAL + }); + + const { data: metricsRx } = useQuery({ + queryKey: [QueriesTopology.GetBytesByProcessPairs, { destProcess: processName }], + queryFn: () => + TopologyController.getMetrics({ + showBytes: true, + showByteRate: true, + showLatency: true, + params: { + ...metricQueryParams, + filterBy: { destProcess: processName } + } + }), + refetchInterval: UPDATE_INTERVAL + }); + + const clients = TopologyController.addMetricsToProcessPairs({ + processesPairs: processesPairsRxData, + metrics: metricsRx, + prometheusKey: 'sourceProcess', + processPairsKey: 'sourceName' + }); + + const servers = TopologyController.addMetricsToProcessPairs({ + processesPairs: processesPairsTxData, + metrics: metricsTx, + prometheusKey: 'destProcess', + processPairsKey: 'destinationName' + }); + const processesPairsRxReverse = - (processesPairsRxData || []).map((processPairsData) => ({ + (clients || []).map((processPairsData) => ({ ...processPairsData, sourceId: processPairsData.destinationId, sourceName: processPairsData.destinationName, @@ -50,18 +101,29 @@ const ProcessPairs: FC = function ({ process: { identity: pro destinationId: processPairsData.sourceId })) || []; - const TCPServers = (processesPairsTxData || []).filter(({ protocol }) => protocol === AvailableProtocols.Tcp); + const TCPServers = (servers || []).filter(({ protocol }) => protocol === AvailableProtocols.Tcp); const TCPClients = processesPairsRxReverse.filter(({ protocol }) => protocol === AvailableProtocols.Tcp); - const HTTPServers = (processesPairsTxData || []).filter( + const HTTPServers = (servers || []).filter( ({ protocol }) => protocol === AvailableProtocols.Http || protocol === AvailableProtocols.Http2 ); const HTTPClients = processesPairsRxReverse.filter( ({ protocol }) => protocol === AvailableProtocols.Http || protocol === AvailableProtocols.Http2 ); - const remoteServers = (processesPairsTxData || []).filter(({ protocol }) => protocol === undefined); + const remoteServers = (servers || []).filter(({ protocol }) => protocol === undefined); const remoteClients = processesPairsRxReverse.filter(({ protocol }) => protocol === undefined); + const CustomProcessPairCellsWithLinkDetail = { + ...CustomProcessPairCells, + viewDetailsLinkCell: ({ data }: LinkCellProps) => ( + + view pairs + + ) + }; + return ( {!!TCPClients.length && ( @@ -72,7 +134,7 @@ const ProcessPairs: FC = function ({ process: { identity: pro rows={TCPClients} pagination={true} paginationPageSize={SMALL_PAGINATION_SIZE} - customCells={CustomProcessPairCells} + customCells={CustomProcessPairCellsWithLinkDetail} /> )} @@ -84,7 +146,7 @@ const ProcessPairs: FC = function ({ process: { identity: pro rows={TCPServers} pagination={true} paginationPageSize={SMALL_PAGINATION_SIZE} - customCells={CustomProcessPairCells} + customCells={CustomProcessPairCellsWithLinkDetail} /> )} @@ -96,7 +158,7 @@ const ProcessPairs: FC = function ({ process: { identity: pro rows={HTTPClients} pagination={true} paginationPageSize={SMALL_PAGINATION_SIZE} - customCells={CustomProcessPairCells} + customCells={CustomProcessPairCellsWithLinkDetail} /> )} @@ -108,7 +170,7 @@ const ProcessPairs: FC = function ({ process: { identity: pro rows={HTTPServers} pagination={true} paginationPageSize={SMALL_PAGINATION_SIZE} - customCells={CustomProcessPairCells} + customCells={CustomProcessPairCellsWithLinkDetail} /> )} @@ -120,7 +182,7 @@ const ProcessPairs: FC = function ({ process: { identity: pro rows={remoteClients} pagination={true} paginationPageSize={SMALL_PAGINATION_SIZE} - customCells={CustomProcessPairCells} + customCells={CustomProcessPairCellsWithLinkDetail} /> )} @@ -132,7 +194,7 @@ const ProcessPairs: FC = function ({ process: { identity: pro rows={remoteServers} pagination={true} paginationPageSize={SMALL_PAGINATION_SIZE} - customCells={CustomProcessPairCells} + customCells={CustomProcessPairCellsWithLinkDetail} /> )} diff --git a/src/pages/Processes/views/Process.tsx b/src/pages/Processes/views/Process.tsx index d4059a08f..c7b4a1b4b 100644 --- a/src/pages/Processes/views/Process.tsx +++ b/src/pages/Processes/views/Process.tsx @@ -1,7 +1,7 @@ import { useState, MouseEvent as ReactMouseEvent } from 'react'; import { Badge, Tab, Tabs, TabTitleText } from '@patternfly/react-core'; -import { useQuery } from '@tanstack/react-query'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; import { useParams, useSearchParams } from 'react-router-dom'; import { RESTApi } from '@API/REST.api'; @@ -20,7 +20,7 @@ const Process = function () { const [searchParams, setSearchParams] = useSearchParams(); const { id } = useParams() as { id: string }; - const { id: processId } = getIdAndNameFromUrlParams(id); + const { id: processId, name: processName } = getIdAndNameFromUrlParams(id); const type = searchParams.get('type') || ProcessesLabels.Overview; const [tabSelected, setTabSelected] = useState(type); @@ -37,18 +37,21 @@ const Process = function () { const { data: process } = useQuery({ queryKey: [QueriesProcesses.GetProcess, processId], - queryFn: () => RESTApi.fetchProcess(processId) + queryFn: () => RESTApi.fetchProcess(processId), + placeholderData: keepPreviousData }); const { data: clientPairs } = useQuery({ queryKey: [QueriesProcesses.GetProcessPairs, clientPairsQueryParams], queryFn: () => RESTApi.fetchProcessesPairs(clientPairsQueryParams), + placeholderData: keepPreviousData, refetchInterval: UPDATE_INTERVAL }); const { data: serverPairs } = useQuery({ queryKey: [QueriesProcesses.GetProcessPairs, serverPairsQueryParams], queryFn: () => RESTApi.fetchProcessesPairs(serverPairsQueryParams), + placeholderData: keepPreviousData, refetchInterval: UPDATE_INTERVAL }); @@ -91,7 +94,7 @@ const Process = function () { return ( } mainContentChildren={ diff --git a/src/pages/Processes/views/ProcessPair.tsx b/src/pages/Processes/views/ProcessPair.tsx index 21b8cda80..d90f892b8 100644 --- a/src/pages/Processes/views/ProcessPair.tsx +++ b/src/pages/Processes/views/ProcessPair.tsx @@ -1,38 +1,24 @@ import { useCallback, useState, MouseEvent as ReactMouseEvent } from 'react'; -import { - Bullseye, - Card, - CardBody, - Grid, - GridItem, - Icon, - Stack, - StackItem, - Tab, - Tabs, - TabTitleText -} from '@patternfly/react-core'; -import { LongArrowAltRightIcon, ResourcesEmptyIcon } from '@patternfly/react-icons'; +import { Card, CardBody, Stack, StackItem, Tab, Tabs, TabTitleText } from '@patternfly/react-core'; +import { ResourcesEmptyIcon } from '@patternfly/react-icons'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; import { useParams, useSearchParams } from 'react-router-dom'; import { RESTApi } from '@API/REST.api'; import { AvailableProtocols, SortDirection, TcpStatus } from '@API/REST.enum'; -import { VarColors } from '@config/colors'; import { DEFAULT_PAGINATION_SIZE, UPDATE_INTERVAL } from '@config/config'; import { getTestsIds } from '@config/testIds'; import EmptyData from '@core/components/EmptyData'; -import LinkCell from '@core/components/LinkCell'; import { getIdAndNameFromUrlParams } from '@core/utils/getIdAndNameFromUrlParams'; import MainContainer from '@layout/MainContainer'; import FlowPairs from '@pages/shared/FlowPairs'; import { TopologyRoutesPaths, TopologyURLQueyParams, TopologyViews } from '@pages/Topology/Topology.enum'; -import { ProcessResponse, RequestOptions } from 'API/REST.interfaces'; +import { RequestOptions } from 'API/REST.interfaces'; -import Details from '../components/Details'; +import ProcessPairDetails from '../components/ProcessPairDetails'; import { activeTcpColumns, httpColumns, oldTcpColumns } from '../Processes.constants'; -import { ProcessesLabels, ProcessesRoutesPaths, QueriesProcesses } from '../Processes.enum'; +import { ProcessesLabels, QueriesProcesses } from '../Processes.enum'; const TAB_1_KEY = 'liveConnections'; const TAB_2_KEY = 'connections'; @@ -69,6 +55,7 @@ const initPaginatedOldConnectionsQueryParams: RequestOptions = { const ProcessPairs = function () { const { processPair } = useParams() as { processPair: string }; const [searchParams, setSearchParams] = useSearchParams(); + const { id: processPairId, protocol } = getIdAndNameFromUrlParams(processPair); const type = searchParams.get('type') || TAB_1_KEY; const ids = processPairId?.split('-to-') || []; @@ -93,16 +80,6 @@ const ProcessPairs = function () { initPaginatedActiveConnectionsQueryParams ); - const { data: source } = useQuery({ - queryKey: [QueriesProcesses.GetProcess, sourceId], - queryFn: () => RESTApi.fetchProcess(sourceId) - }); - - const { data: destination } = useQuery({ - queryKey: [QueriesProcesses.GetDestination, destinationId], - queryFn: () => RESTApi.fetchProcess(destinationId) - }); - const { data: http2RequestsData } = useQuery({ queryKey: [QueriesProcesses.GetFlowPair, http2QueryParamsPaginated, processPairId], queryFn: () => @@ -172,10 +149,6 @@ const ProcessPairs = function () { setSearchParams({ type: tabIndex as string }); } - if (!source || !destination) { - return null; - } - if (protocol === AvailableProtocols.Http && !httpRequestsData) { return null; } @@ -200,42 +173,6 @@ const ProcessPairs = function () { const activeConnections = activeConnectionsData?.results || []; const activeConnectionsCount = activeConnectionsData?.timeRangeCount || 0; - const ClientServerDescription = function () { - return ( - - -
({ - data: source, - value: source.name, - link: `${ProcessesRoutesPaths.Processes}/${source.name}@${sourceId}` - })} - /> - - - - - - - - - - - -
({ - data: destination, - value: destination.name, - link: `${ProcessesRoutesPaths.Processes}/${destination.name}@${destinationId}` - })} - /> - - - ); - }; - const NoDataCard = function () { return ( @@ -258,7 +195,7 @@ const ProcessPairs = function () { mainContentChildren={ - + {!activeConnectionsCount && !oldConnectionsCount && !http2Requests.length && !httpRequests.length && ( diff --git a/src/pages/Services/Services.constants.ts b/src/pages/Services/Services.constants.ts index 414816d88..b94eebb45 100644 --- a/src/pages/Services/Services.constants.ts +++ b/src/pages/Services/Services.constants.ts @@ -4,7 +4,6 @@ import LinkCell from '@core/components/LinkCell'; import { LinkCellProps } from '@core/components/LinkCell/LinkCell.interfaces'; import { sankeyMetricOptions } from '@core/components/SKSanckeyChart/SkSankey.constants'; import { SKColumn } from '@core/components/SkTable/SkTable.interfaces'; -import { formatByteRate } from '@core/utils/formatBytes'; import { timeAgo } from '@core/utils/timeAgo'; import { ProcessesLabels } from '@pages/Processes/Processes.enum'; import { httpFlowPairsColumns, tcpFlowPairsColumns } from '@pages/shared/FlowPairs/FlowPair.constants'; @@ -73,7 +72,7 @@ export const tcpServerColumns = [ { name: ProcessesLabels.ByteRateRx, prop: 'byteRate' as keyof ProcessResponse, - format: formatByteRate + customCellName: 'ByteRateFormatCell' }, { name: ProcessesLabels.Created, diff --git a/src/pages/Topology/Topology.interfaces.ts b/src/pages/Topology/Topology.interfaces.ts index da81b07e0..a65f1501a 100644 --- a/src/pages/Topology/Topology.interfaces.ts +++ b/src/pages/Topology/Topology.interfaces.ts @@ -1,6 +1,7 @@ import { ModelStyle } from '@antv/g6'; -import { PrometheusApiSingleResult } from '@API/Prometheus.interfaces'; +import { PrometheusApiSingleResult, PrometheusLabels } from '@API/Prometheus.interfaces'; +import { ProcessPairsResponse, ProcessResponse } from '@API/REST.interfaces'; export interface Entity { id: string; @@ -27,6 +28,7 @@ export interface TopologyConfigMetrics { fetchBytes: { groupBy: string }; fetchByteRate: { groupBy: string }; fetchLatency: { groupBy: string }; + filterBy?: PrometheusLabels; }; } @@ -46,3 +48,17 @@ export interface DisplayOptions { showLinkLabelReverse?: boolean; rotateLabel?: boolean; } + +export interface TopologyModalProps { + ids: string; + items: ProcessResponse[] | ProcessPairsResponse[]; + modalType: 'process' | 'processPair' | undefined; + onClose?: Function; +} + +export interface ProcessPairsWithMetrics { + processesPairs?: ProcessPairsResponse[]; + metrics?: TopologyMetrics; + prometheusKey: 'sourceProcess' | 'destProcess'; + processPairsKey: 'sourceName' | 'destinationName'; +} diff --git a/src/pages/Topology/__tests__/TopologyProcesses.spec.tsx b/src/pages/Topology/__tests__/TopologyProcesses.spec.tsx index ce8924127..2fcd350e7 100644 --- a/src/pages/Topology/__tests__/TopologyProcesses.spec.tsx +++ b/src/pages/Topology/__tests__/TopologyProcesses.spec.tsx @@ -16,6 +16,7 @@ import LoadingPage from '@pages/shared/Loading'; import TopologyProcesses from '../components/TopologyProcesses'; import { TopologyLabels } from '../Topology.enum'; +import { TopologyModalProps } from '../Topology.interfaces'; const navigate = jest.fn(); @@ -55,6 +56,22 @@ const MockGraphComponent: FC = memo( }) ); +const MockTopologyModalComponent: FC = function ({ ids, items, onClose, modalType }) { + return ( + <> + {!!modalType && ( +
+
Modal is open
+
{`Modal type: ${modalType}`}
+
{`Selected ids: ${ids}`}
+
{`Selected items: ${items}`}
+
+ )} + + + ); +}; + describe('Begin testing the Topology component', () => { let server: Server; @@ -69,6 +86,7 @@ describe('Begin testing the Topology component', () => { serviceIds={[serviceNameSelected]} id={processesResults[2].name} GraphComponent={MockGraphComponent} + ModalComponent={MockTopologyModalComponent} /> @@ -124,14 +142,11 @@ describe('Begin testing the Topology component', () => { }); it('should clicking on a node', async () => { - jest.spyOn(router, 'useNavigate').mockImplementation(() => navigate); - await waitForElementToBeRemoved(() => screen.queryByTestId(getTestsIds.loadingView()), { timeout: waitForElementToBeRemovedTimeout }); fireEvent.click(screen.getByText('onClickNode')); - expect(navigate).toHaveBeenCalledTimes(1); }); it('should clicking on a edge', async () => { @@ -140,10 +155,6 @@ describe('Begin testing the Topology component', () => { }); fireEvent.click(screen.getByText('onClickEdge')); - expect(navigate).toHaveBeenCalledTimes(1); - - fireEvent.click(screen.getByText('onClickCombo')); - expect(navigate).toHaveBeenCalledTimes(2); }); it('should clicking on a combo', async () => { diff --git a/src/pages/Topology/components/DisplaySelect.tsx b/src/pages/Topology/components/DisplayOptions.tsx similarity index 98% rename from src/pages/Topology/components/DisplaySelect.tsx rename to src/pages/Topology/components/DisplayOptions.tsx index e3e8114eb..76b9f4975 100644 --- a/src/pages/Topology/components/DisplaySelect.tsx +++ b/src/pages/Topology/components/DisplayOptions.tsx @@ -7,7 +7,7 @@ import { SHOW_DATA_LINKS, SHOW_ROUTER_LINKS } from '../Topology.constants'; import { TopologyLabels } from '../Topology.enum'; import { DisplaySelectProps } from '../Topology.interfaces'; -const DisplaySelect: FC<{ +const DisplayOptions: FC<{ options: DisplaySelectProps[]; defaultSelected?: string[]; onSelect: Function; @@ -84,4 +84,4 @@ const DisplaySelect: FC<{ ); }; -export default DisplaySelect; +export default DisplayOptions; diff --git a/src/pages/Topology/components/TopologyModal.tsx b/src/pages/Topology/components/TopologyModal.tsx new file mode 100644 index 000000000..2f89713c9 --- /dev/null +++ b/src/pages/Topology/components/TopologyModal.tsx @@ -0,0 +1,109 @@ +import { FC, Suspense, useCallback, useState } from 'react'; + +import { Bullseye, Button, Flex, Modal, Spinner, Text, Title } from '@patternfly/react-core'; +import { Link } from 'react-router-dom'; + +import { ProcessPairsResponse, ProcessResponse } from '@API/REST.interfaces'; +import Details from '@pages/Processes/components/Details'; +import ProcessPairs from '@pages/Processes/components/ProcessesPairs'; +import ProcessPairDetails from '@pages/Processes/components/ProcessPairDetails'; +import { ProcessesLabels, ProcessesRoutesPaths } from '@pages/Processes/Processes.enum'; + +import { TopologyModalProps } from '../Topology.interfaces'; + +const TopologyModal: FC = function ({ ids, items, modalType, onClose }) { + const [index, setIndex] = useState(0); + + const handleClose = useCallback(() => { + setIndex(0); + onClose?.(); + }, [onClose]); + + const idsArray = ids?.split('~'); + const hasMoreElements = idsArray?.length > 1; + + let Header = null; + let Body = null; + const itemsSelected = items.filter(({ identity }) => idsArray?.includes(identity)); + + if (modalType === 'process') { + const item = itemsSelected[index] as ProcessResponse; + + Header = ( + + {item?.name} + + ); + + Body = ( + + + + } + > +
+ + + ); + } + + if (modalType === 'processPair') { + const item = itemsSelected[index] as ProcessPairsResponse; + + Header = ( + + + {item?.sourceName} + to + {item.destinationName} + + + ); + + Body = ( + }> + + + ); + } + + return ( + + {hasMoreElements && ( + + )} + {Header} + {hasMoreElements && ( + + )} + + {hasMoreElements && {`${index + 1} of ${idsArray?.length}`}} + + } + > + {Body} + + ); +}; + +export default TopologyModal; diff --git a/src/pages/Topology/components/TopologyProcesses.tsx b/src/pages/Topology/components/TopologyProcesses.tsx index 3b6866c61..7c23b8d20 100644 --- a/src/pages/Topology/components/TopologyProcesses.tsx +++ b/src/pages/Topology/components/TopologyProcesses.tsx @@ -8,16 +8,8 @@ import { AlertVariant, Button, Divider, - List, - ListItem, - Modal, - ModalVariant, - Panel, - PanelHeader, - PanelMainBody, Stack, StackItem, - Title, Toolbar, ToolbarContent, ToolbarGroup, @@ -30,7 +22,6 @@ import { useQueries, useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { RESTApi } from '@API/REST.api'; -import { ProcessResponse } from '@API/REST.interfaces'; import { UPDATE_INTERVAL } from '@config/config'; import EmptyData from '@core/components/EmptyData'; import { @@ -42,12 +33,13 @@ import { } from '@core/components/Graph/Graph.interfaces'; import GraphReactAdaptor from '@core/components/Graph/ReactAdaptor'; import NavigationViewLink from '@core/components/NavigationViewLink'; -import { ProcessesLabels, ProcessesRoutesPaths, QueriesProcesses } from '@pages/Processes/Processes.enum'; +import { ProcessesRoutesPaths, QueriesProcesses } from '@pages/Processes/Processes.enum'; import { SitesRoutesPaths, QueriesSites } from '@pages/Sites/Sites.enum'; +import DisplayOptions from './DisplayOptions'; import DisplayResource from './DisplayResources'; -import DisplaySelect from './DisplaySelect'; import DisplayServices from './DisplayServices'; +import TopologyModal from './TopologyModal'; import { TopologyController, groupNodes, groupEdges as groupEdges } from '../services'; import { GROUP_NODES_COMBO_GROUP, @@ -61,6 +53,7 @@ import { displayOptionsForProcesses } from '../Topology.constants'; import { TopologyLabels, QueriesTopology } from '../Topology.enum'; +import { TopologyModalProps } from '../Topology.interfaces'; const ZOOM_CACHE_KEY = 'process'; const DISPLAY_OPTIONS = 'display-options'; @@ -75,25 +68,31 @@ const remoteProcessesQueryParams = { processRole: 'remote' }; +const metricQueryParams = { + fetchBytes: { groupBy: 'destProcess, sourceProcess, direction' }, + fetchByteRate: { groupBy: 'destProcess, sourceProcess, direction' }, + fetchLatency: { groupBy: 'sourceProcess, destProcess' } +}; + const TopologyProcesses: FC<{ serviceIds?: string[]; id?: string; GraphComponent?: ComponentType; -}> = function ({ serviceIds, id: processId, GraphComponent = GraphReactAdaptor }) { + ModalComponent?: ComponentType; +}> = function ({ serviceIds, id: itemId, GraphComponent = GraphReactAdaptor, ModalComponent = TopologyModal }) { const navigate = useNavigate(); + const configuration = TopologyController.loadDisplayOptions(DISPLAY_OPTIONS, DEFAULT_DISPLAY_OPTIONS_ENABLED); const [nodes, setNodes] = useState([]); const [links, setLinks] = useState([]); const [groups, setGroups] = useState([]); - const [processIdSelected, setProcessIdSelected] = useState(processId); - const [serviceIdsSelected, setServiceIdsSelected] = useState(serviceIds); + const [itemIdSelected, setItemIdSelected] = useState(itemId); //process or link id - const [isProcessModalOpen, setIsModalOpen] = useState(false); - const [isProcessPairModalOpen, setIsProcessPairModalOpen] = useState(false); + const [serviceIdsSelected, setServiceIdsSelected] = useState(serviceIds); - const configuration = TopologyController.loadDisplayOptions(DISPLAY_OPTIONS, DEFAULT_DISPLAY_OPTIONS_ENABLED); const [displayOptionsSelected, setDisplayOptionsSelected] = useState(configuration); const [alerts, setAlerts] = useState[]>([]); + const [modalType, setModalType] = useState<'process' | 'processPair' | undefined>(); const graphRef = useRef(); @@ -139,11 +138,7 @@ const TopologyProcesses: FC<{ showBytes: isDisplayOptionActive(SHOW_LINK_BYTES), showByteRate: isDisplayOptionActive(SHOW_LINK_BYTERATE), showLatency: isDisplayOptionActive(SHOW_LINK_LATENCY), - params: { - fetchBytes: { groupBy: 'destProcess, sourceProcess,direction' }, - fetchByteRate: { groupBy: 'destProcess, sourceProcess,direction' }, - fetchLatency: { groupBy: 'sourceProcess, destProcess' } - } + params: metricQueryParams }), refetchInterval: UPDATE_INTERVAL }); @@ -160,62 +155,32 @@ const TopologyProcesses: FC<{ addAlert(message, 'info', getUniqueId()); }, []); - const handleProcessModalToggle = useCallback(() => { - setIsModalOpen(!isProcessModalOpen); - }, [isProcessModalOpen]); - - const handleProcessPairModalToggle = useCallback(() => { - setIsProcessPairModalOpen(!isProcessPairModalOpen); - }, [isProcessPairModalOpen]); - - const handleGetSelectedGroup = useCallback( + const handleGetSelectedSite = useCallback( ({ id, label }: GraphCombo) => { navigate(`${SitesRoutesPaths.Sites}/${label}@${id}`); }, [navigate] ); - const handleGetSelectedNode = useCallback( - ({ id }: { id: string }) => { - const processes = [...externalProcesses!, ...remoteProcesses!]; - - if (id.split('~').length > 1) { - setProcessIdSelected(id); - handleProcessModalToggle(); - - return; - } + const handleResourceSelected = useCallback((id?: string) => { + setItemIdSelected(id); + graphRef?.current?.focusItem(id); + }, []); - const process = processes.find(({ identity }) => identity === id); - navigate(`${ProcessesRoutesPaths.Processes}/${process?.name}@${id}`); + const handleGetSelectedEdge = useCallback( + ({ id }: { id: string }) => { + handleResourceSelected(id); + setModalType('processPair'); }, - [externalProcesses, remoteProcesses, navigate, handleProcessModalToggle] + [handleResourceSelected] ); - const handleGetSelectedEdge = useCallback( + const handleGetSelectedNode = useCallback( ({ id }: { id: string }) => { - if (id.split('~').length > 1) { - setProcessIdSelected(id); - handleProcessPairModalToggle(); - - return; - } - - if (externalProcesses && remoteProcesses) { - const [sourceId] = id.split('-to-'); - const processes = [...externalProcesses, ...remoteProcesses]; - - const sourceProcess = processes?.find(({ identity }) => identity === sourceId) as ProcessResponse; - const protocol = processesPairs?.find(({ identity }) => identity === id)?.protocol; - - if (sourceProcess) { - navigate( - `${ProcessesRoutesPaths.Processes}/${sourceProcess.name}@${sourceProcess.identity}/${ProcessesLabels.ProcessPairs}@${id}@${protocol}` - ); - } - } + handleResourceSelected(id); + setModalType('process'); }, - [externalProcesses, remoteProcesses, handleProcessPairModalToggle, processesPairs, navigate] + [handleResourceSelected] ); const handleDisplayOptionSelected = useCallback((options: string[]) => { @@ -226,16 +191,19 @@ const TopologyProcesses: FC<{ localStorage.setItem(DISPLAY_OPTIONS, JSON.stringify(options)); }, []); - const handleProcessSelected = useCallback((id?: string) => { - setProcessIdSelected(id); - graphRef?.current?.focusItem(id); - }, []); + const handleCloseModal = useCallback(() => { + handleResourceSelected(undefined); + setModalType(undefined); + }, [handleResourceSelected]); - const handleServiceSelected = useCallback((ids: string[]) => { - setServiceIdsSelected(ids); - setProcessIdSelected(undefined); - setTimeout(() => graphRef?.current?.fitView(), 100); - }, []); + const handleServiceSelected = useCallback( + (ids: string[]) => { + setServiceIdsSelected(ids); + handleResourceSelected(undefined); + setTimeout(() => graphRef?.current?.fitView(), 100); + }, + [handleResourceSelected] + ); const handleSaveTopology = useCallback(() => { localStorage.setItem(SERVICE_OPTIONS, JSON.stringify(serviceIdsSelected)); @@ -378,9 +346,8 @@ const TopologyProcesses: FC<{ return option; }); - const nodeIdSelected = nodes.find( - ({ id }) => id.split('~').includes(processIdSelected || '') || processIdSelected === id - )?.id; + const nodeIdSelected = nodes.find(({ id }) => id.split('~').includes(itemIdSelected || '') || itemIdSelected === id) + ?.id; const TopologyToolbar = ( @@ -392,13 +359,13 @@ const TopologyProcesses: FC<{ ({ name: node.label, identity: node.id }))} /> - )} {!nodes.length && } - - {alerts.map(({ key, title }) => ( - removeAlert(key as Key)} />} - /> - ))} - - - {processIdSelected?.split('~').map((id) => { - const processPair = processesPairs!.find(({ identity }) => identity === id); - - if (!processPair) { - return null; - } - - return ( -
- -
- ); - })} -
- - - - {processIdSelected?.split('~').map((id) => { - const processes = [...remoteProcesses!, ...externalProcesses!]; - const process = processes.find(({ identity }) => identity === id); - const clientPairs = processesPairs - ?.filter(({ destinationId }) => destinationId === id) - .flatMap(({ sourceId }) => [sourceId]); - - const targetPairs = processesPairs - ?.filter(({ sourceId }) => sourceId === id) - .flatMap(({ destinationId }) => [destinationId]); - - const clients = processes.filter(({ identity }) => clientPairs?.includes(identity)); - const targets = processes.filter(({ identity }) => targetPairs?.includes(identity)); - - if (!process) { - return null; - } - - return ( - - - - - - - {!!clients.length && ( - <> - Clients - - {clients.map(({ identity, name, sourceHost, hostName }) => ( - - - - ))} - - - )} - {!!targets.length && ( - <> - Send data to - - {targets.map(({ identity, name, sourceHost, hostName }) => ( - - - - ))} - - - )} - - - - ); - })} - - + + {alerts.map(({ key, title }) => ( + removeAlert(key as Key)} />} + /> + ))} + + ); }; diff --git a/src/pages/Topology/components/TopologySite.tsx b/src/pages/Topology/components/TopologySite.tsx index 15ba2b127..4a2c5ac60 100644 --- a/src/pages/Topology/components/TopologySite.tsx +++ b/src/pages/Topology/components/TopologySite.tsx @@ -18,8 +18,8 @@ import GraphReactAdaptor from '@core/components/Graph/ReactAdaptor'; import NavigationViewLink from '@core/components/NavigationViewLink'; import { QueriesSites, SitesRoutesPaths } from '@pages/Sites/Sites.enum'; +import DisplayOptions from './DisplayOptions'; import DisplayResource from './DisplayResources'; -import DisplaySelect from './DisplaySelect'; import { TopologyController } from '../services'; import { displayOptionsForSites, @@ -233,7 +233,7 @@ const TopologySite: FC<{ id?: string | null; GraphComponent?: ComponentType
- => { try { const [bytesByProcessPairs, byteRateByProcessPairs, latencyByProcessPairs] = await Promise.all([ - showBytes ? PrometheusApi.fetchAllProcessPairsBytes(params.fetchBytes.groupBy) : [], - showByteRate ? PrometheusApi.fetchAllProcessPairsByteRates(params.fetchByteRate.groupBy) : [], - showLatency ? PrometheusApi.fetchAllProcessPairsLatencies(params.fetchLatency.groupBy) : [] + showBytes ? PrometheusApi.fetchAllProcessPairsBytes(params.fetchBytes.groupBy, params.filterBy) : [], + showByteRate ? PrometheusApi.fetchAllProcessPairsByteRates(params.fetchByteRate.groupBy, params.filterBy) : [], + showLatency ? PrometheusApi.fetchAllProcessPairsLatencies(params.fetchLatency.groupBy, params.filterBy) : [] ]); return { bytesByProcessPairs, byteRateByProcessPairs, latencyByProcessPairs }; @@ -134,6 +140,36 @@ export const TopologyController = { ); }, + addMetricsToProcessPairs: ({ processesPairs, metrics, prometheusKey, processPairsKey }: ProcessPairsWithMetrics) => { + const getPairsMap = (metricPairs: PrometheusApiSingleResult[] | undefined, key: string) => + (metricPairs || []).reduce( + (acc, { metric, value }) => { + { + if (metric.sourceProcess === metric.destProcess) { + // When the source and destination are identical, we should avoid displaying the reverse metric. Instead, we should present the cumulative sum of all directions as a single value. + acc[`${metric[key]}`] = (Number(acc[`${metric[key]}`]) || 0) + Number(value[1]); + } else { + acc[`${metric[key]}`] = Number(value[1]); + } + } + + return acc; + }, + {} as Record + ); + + const txBytesByPairsMap = getPairsMap(metrics?.bytesByProcessPairs, prometheusKey); + const txByteRateByPairsMap = getPairsMap(metrics?.byteRateByProcessPairs, prometheusKey); + const txLatencyByPairsMap = getPairsMap(metrics?.latencyByProcessPairs, prometheusKey); + + return processesPairs?.map((processPairsData) => ({ + ...processPairsData, + bytes: txBytesByPairsMap[processPairsData[processPairsKey]] || 0, + byteRate: txByteRateByPairsMap[processPairsData[processPairsKey]] || 0, + latency: txLatencyByPairsMap[processPairsData[processPairsKey]] || 0 + })); + }, + addMetricsToEdges: ( edges: GraphEdge[], metricSourceLabel: string, // Prometheus metric label to compare with the metricDestLabel diff --git a/src/pages/shared/FlowPairs/FlowPair.constants.ts b/src/pages/shared/FlowPairs/FlowPair.constants.ts index ddd0af6bd..c7e2c7767 100644 --- a/src/pages/shared/FlowPairs/FlowPair.constants.ts +++ b/src/pages/shared/FlowPairs/FlowPair.constants.ts @@ -38,7 +38,7 @@ export const flowPairsComponentsTable = { type: 'process', link: `${ProcessesRoutesPaths.Processes}/${props.data.counterFlow.processName}@${props.data.counterFlow.process}` }), - ClientServerLatencyCell: (props: LinkCellProps) => + TcpTTFB: (props: LinkCellProps) => formatLatency(props.data.counterFlow.latency + props.data.forwardFlow.latency), DurationCell: (props: LinkCellProps) => DurationCell({ ...props, endTime: props.data.endTime || Date.now() * 1000, startTime: props.data.startTime }), @@ -73,20 +73,18 @@ export const tcpFlowPairsColumns: SKColumn[] = [ name: FlowPairLabels.TxBytes, prop: 'forwardFlow.octets' as keyof FlowPairsResponse, customCellName: 'ByteFormatCell', - format: formatBytes, modifier: 'nowrap' }, { name: FlowPairLabels.RxBytes, prop: 'counterFlow.octets' as keyof FlowPairsResponse, customCellName: 'ByteFormatCell', - format: formatBytes, modifier: 'nowrap' }, { name: FlowPairLabels.TTFB, columnDescription: 'time elapsed between client and server', - customCellName: 'ClientServerLatencyCell', + customCellName: 'TcpTTFB', modifier: 'nowrap' }, { diff --git a/webpack.common.js b/webpack.common.js index 9c4240cf5..b40b3e548 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -53,7 +53,9 @@ module.exports = { 'process.env.BRAND_APP_LOGO': JSON.stringify(process.env.BRAND_APP_LOGO || ''), 'process.env.APP_VERSION': JSON.stringify(process.env.APP_VERSION) || JSON.stringify(version), 'process.env.COLLECTOR_URL': JSON.stringify(process.env.COLLECTOR_URL || ''), - 'process.env.DISABLE_METRICS': JSON.stringify(process.env.DISABLE_METRICS) + 'process.env.DISABLE_METRICS': JSON.stringify(process.env.DISABLE_METRICS), + 'process.env.MOCK_ITEM_COUNT': JSON.stringify(process.env.MOCK_ITEM_COUNT), + 'process.env.MOCK_DELAY_RESPONSE': JSON.stringify(process.env.MOCK_DELAY_RESPONSE) }), new HtmlWebpackPlugin({ template: path.join(ROOT, '/public/index.html'),