From 0ffb5d7268db6b6e82824e3cc0ad08ef167661dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 10 Dec 2024 18:18:12 +0000 Subject: [PATCH] [Services map] Update redirection/Focus field rework (#2264) * Change service table click to page redirection Signed-off-by: Adam Tackett * remove focus on filtering, bug fixes Signed-off-by: Adam Tackett * add jest test for url redirection on click Signed-off-by: Adam Tackett * remove filter on focus, update focus functionality, add testing Signed-off-by: Adam Tackett * add query filtering back Signed-off-by: Adam Tackett * change url check to use hook state Signed-off-by: Adam Tackett * fix location error Signed-off-by: Adam Tackett * move flyout into hashrouter, remove extra filter Signed-off-by: Adam Tackett * fix applications, address comments Signed-off-by: Adam Tackett * fix flaky application test Signed-off-by: Adam Tackett * make service map snapshop deep Signed-off-by: Adam Tackett * add service map focus cypress testing Signed-off-by: Adam Tackett * add more tests to cover service map load and click Signed-off-by: Shenoy Pratik * add comments to cypress Signed-off-by: Shenoy Pratik * remove only from cypress tests Signed-off-by: Shenoy Pratik * update snapshots Signed-off-by: Shenoy Pratik * adjust cypress test Signed-off-by: Adam Tackett * adjust cypress testw Signed-off-by: Adam Tackett * remove only and comment Signed-off-by: Adam Tackett * switch test back Signed-off-by: Adam Tackett * add dependancy to use effect Signed-off-by: Adam Tackett --------- Signed-off-by: Adam Tackett Signed-off-by: Shenoy Pratik Co-authored-by: Adam Tackett Co-authored-by: Shenoy Pratik (cherry picked from commit c22c69dac4f6fa7977c38d4a9682c3d46841cdda) Signed-off-by: github-actions[bot] --- .../app_analytics_test/app_analytics.spec.js | 3 +- .../trace_analytics_services.spec.js | 66 +- .../__snapshots__/create.test.tsx.snap | 1762 +++++++++-------- .../__snapshots__/flyout.test.tsx.snap | 8 +- .../service_config.test.tsx.snap | 728 ++++--- .../__snapshots__/service_map.test.tsx.snap | 707 +++---- .../plots/__tests__/service_map.test.tsx | 209 +- .../components/common/plots/service_map.tsx | 244 ++- .../__snapshots__/service_view.test.tsx.snap | 2 +- .../__snapshots__/services.test.tsx.snap | 787 +++++--- .../services_table.test.tsx.snap | 4 + .../services/__tests__/service_view.test.tsx | 68 +- .../__tests__/services_table.test.tsx | 52 + .../components/services/service_view.tsx | 17 +- .../components/services/services_content.tsx | 4 + .../components/services/services_table.tsx | 296 +-- .../components/traces/span_detail_panel.tsx | 4 +- public/components/trace_analytics/home.tsx | 4 +- test/constants.ts | 71 +- 19 files changed, 2820 insertions(+), 2216 deletions(-) diff --git a/.cypress/integration/app_analytics_test/app_analytics.spec.js b/.cypress/integration/app_analytics_test/app_analytics.spec.js index ce0ff7820..bee7aba52 100644 --- a/.cypress/integration/app_analytics_test/app_analytics.spec.js +++ b/.cypress/integration/app_analytics_test/app_analytics.spec.js @@ -274,7 +274,7 @@ describe('Viewing application', () => { it('Opens service detail flyout when Service Name is clicked', () => { cy.get('[data-test-subj="app-analytics-serviceTab"]').click(); - cy.get('*[data-test-subj^="service-flyout-action-btntrace"]').eq(0).click(); + cy.get('*[data-test-subj^="service-link"]').eq(0).click(); cy.get('[data-test-subj="serviceDetailFlyoutTitle"]').should('be.visible'); cy.get('[data-test-subj="Number of connected servicesDescriptionList"]').should('contain', '3'); cy.get('[data-text="Errors"]').eq(1).click(); // Selecting errors tab within flyout @@ -297,6 +297,7 @@ describe('Viewing application', () => { }); cy.get('[data-test-subj="euiFlyoutCloseButton"]').click(); cy.get('[data-test-subj="traceDetailFlyout"]').should('not.exist'); + cy.get('[data-test-subj="superDatePickerShowDatesButton"]').click();//added to replace wait cy.get('[title="03f9c770db5ee2f1caac0afc36db49ba"]').click(); cy.get('[data-text="Span list"]').click(); cy.get('[data-test-subj="dataGridRowCell"]').contains('d67c5bb617ba9203').click(); diff --git a/.cypress/integration/trace_analytics_test/trace_analytics_services.spec.js b/.cypress/integration/trace_analytics_test/trace_analytics_services.spec.js index 95ef28e57..8f3121f8a 100644 --- a/.cypress/integration/trace_analytics_test/trace_analytics_services.spec.js +++ b/.cypress/integration/trace_analytics_test/trace_analytics_services.spec.js @@ -160,6 +160,7 @@ describe('Testing Service map', () => { cy.get("[data-test-subj='indexPattern-switch-link']").click(); cy.get("[data-test-subj='data_prepper-mode']").click(); setTimeFilter(); + cy.get('.euiTableRow').should('have.length.greaterThan', 7); //Replaces wait }); it('Render Service map', () => { @@ -171,7 +172,70 @@ describe('Testing Service map', () => { cy.get('[data-text = "Duration"]').click(); cy.contains('100'); cy.get('.euiFormLabel.euiFormControlLayout__prepend').contains('Focus on').should('exist'); - cy.get('[placeholder="Service name"]').focus().type('database{enter}'); + }); + + it('Render the vis-network div and canvas', () => { + // Check the view where ServiceMap component is rendered + cy.get('.euiText.euiText--medium .panel-title').contains('Service map'); + cy.get('.vis-network').should('exist'); + cy.get('.vis-network canvas').should('exist'); + + // Check the canvas is not empty + cy.get('.vis-network canvas') + .should('have.attr', 'style') + .and('include', 'position: relative') + .and('include', 'touch-action: none') + .and('include', 'user-select: none') + .and('include', 'width: 100%') + .and('include', 'height: 100%'); + + cy.get('.vis-network canvas').should('have.attr', 'width').and('not.eq', '0'); + cy.get('.vis-network canvas').should('have.attr', 'height').and('not.eq', '0'); + }); + + it('Click on a node to see the details', () => { + cy.get('.euiText.euiText--medium .panel-title').contains('Service map'); + cy.get('.vis-network canvas').should('exist'); + + // ensure rendering is complete before node click, replace wait + cy.get('[data-text="Errors"]').click(); + cy.contains('60%'); + cy.get('[data-text="Duration"]').click(); + cy.contains('100'); + + // clicks on payment node + cy.get('.vis-network canvas').click(707, 388); + // checks the duration in node details popover + cy.get('.euiText.euiText--small').contains('Average duration: 216.43ms').should('exist'); + }); + + it('Tests focus functionality in Service map', () => { + cy.get('.euiText.euiText--medium .panel-title').contains('Service map'); + cy.get('[data-test-subj="latency"]').should('exist'); + cy.get('.ytitle').contains('Average duration (ms)'); + + // Test metric selection functionality + cy.get('[data-text="Errors"]').click(); + cy.contains('60%'); + cy.get('[data-text="Duration"]').click(); + cy.contains('100'); + + // Focus on "order" by selecting the first option + cy.get('.euiFormLabel.euiFormControlLayout__prepend').contains('Focus on').should('exist'); + cy.get('[placeholder="Service name"]').click(); + cy.get('.euiSelectableList__list li').eq(0).click(); + + // Verify the service map updates and focus is applied + cy.get('.euiFormLabel.euiFormControlLayout__prepend').contains('Focus on').should('exist'); + cy.get('[placeholder="order"]').click(); + cy.get('.euiSelectableList__list li').should('have.length', 4); // Focused view with 4 options + + // Refresh to reset the focus + cy.get('[data-test-subj="serviceMapRefreshButton"]').click(); + + // Verify the service map is reset to the original state + cy.get('[placeholder="Service name"]').should('have.value', ''); + cy.get('.euiSelectableList__list li').should('have.length', 8); // Original 8 options }); }); diff --git a/public/components/application_analytics/__tests__/__snapshots__/create.test.tsx.snap b/public/components/application_analytics/__tests__/__snapshots__/create.test.tsx.snap index c3b15e282..2785161b1 100644 --- a/public/components/application_analytics/__tests__/__snapshots__/create.test.tsx.snap +++ b/public/components/application_analytics/__tests__/__snapshots__/create.test.tsx.snap @@ -660,67 +660,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -1940,67 +1946,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -3159,67 +3171,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -4353,67 +4371,73 @@ Object {
-
-
- +
- - +
+ - - - + + +
-
- -
+ + +
+ @@ -5640,67 +5664,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -6834,67 +6864,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -8048,67 +8084,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -9203,67 +9245,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -10420,67 +10468,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -11580,67 +11634,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -12831,67 +12891,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -14025,67 +14091,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -15244,67 +15316,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -16438,67 +16516,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -17695,67 +17779,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ @@ -18975,67 +19065,73 @@ Object {
-
-
- +
- +
- - + + +
-
- -
+ + +
+ diff --git a/public/components/application_analytics/__tests__/__snapshots__/flyout.test.tsx.snap b/public/components/application_analytics/__tests__/__snapshots__/flyout.test.tsx.snap index c4199b133..874e18e43 100644 --- a/public/components/application_analytics/__tests__/__snapshots__/flyout.test.tsx.snap +++ b/public/components/application_analytics/__tests__/__snapshots__/flyout.test.tsx.snap @@ -1221,7 +1221,7 @@ exports[`Trace Detail Render Flyout component render trace detail 1`] = ` "yref": "paper", }, ], - "width": undefined, + "width": 412, "xaxis": Object { "color": "#91989c", "range": Array [ @@ -1287,7 +1287,7 @@ exports[`Trace Detail Render Flyout component render trace detail 1`] = ` }, ], "showlegend": false, - "width": undefined, + "width": 412, "xaxis": Object { "color": "#91989c", "range": Array [ @@ -1357,7 +1357,7 @@ exports[`Trace Detail Render Flyout component render trace detail 1`] = ` }, "paper_bgcolor": "rgba(0, 0, 0, 0)", "plot_bgcolor": "rgba(0, 0, 0, 0)", - "width": undefined, + "width": 412, "xaxis": Object { "color": "#91989c", "range": Array [ @@ -1409,7 +1409,7 @@ exports[`Trace Detail Render Flyout component render trace detail 1`] = ` "paper_bgcolor": "rgba(0, 0, 0, 0)", "plot_bgcolor": "rgba(0, 0, 0, 0)", "showlegend": false, - "width": undefined, + "width": 412, "xaxis": Object { "color": "#91989c", "range": Array [ diff --git a/public/components/application_analytics/__tests__/__snapshots__/service_config.test.tsx.snap b/public/components/application_analytics/__tests__/__snapshots__/service_config.test.tsx.snap index f8ad6ce5f..b564b3c45 100644 --- a/public/components/application_analytics/__tests__/__snapshots__/service_config.test.tsx.snap +++ b/public/components/application_analytics/__tests__/__snapshots__/service_config.test.tsx.snap @@ -1004,27 +1004,35 @@ exports[`Service Config component renders empty service config 1`] = ` - } - aria-controls="service-select-dropdown" - compressed={true} - fullWidth={false} - incremental={false} - isClearable={true} - isInvalid={false} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - placeholder="Service name" - prepend="Focus on" - value="" - /> + + + } + aria-controls="service-select-dropdown" + compressed={true} + disabled={false} + fullWidth={false} + incremental={false} + isClearable={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + placeholder="Service name" + prepend="Focus on" + value="" + /> + } closePopover={[Function]} display="inlineBlock" @@ -1050,169 +1058,195 @@ exports[`Service Config component renders empty service config 1`] = `
- - } - aria-controls="service-select-dropdown" - compressed={true} - fullWidth={false} - incremental={false} - isClearable={true} - isInvalid={false} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - placeholder="Service name" - prepend="Focus on" - value="" + - - } - compressed={true} - fullWidth={false} - icon="search" - isLoading={false} - prepend="Focus on" + -
+ } + aria-controls="service-select-dropdown" + compressed={true} + disabled={false} + fullWidth={false} + incremental={false} + isClearable={true} + isInvalid={false} + isLoading={false} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onFocus={[Function]} + placeholder="Service name" + prepend="Focus on" + value="" > - - - -
- - - - +
+ + +
- - + + +
- - - + + + + + + + + +
+
- -
- - - -
- - + + + + + + + + +
+
+
+ +
@@ -2350,27 +2384,35 @@ exports[`Service Config component renders with one service selected 1`] = ` - } - aria-controls="service-select-dropdown" - compressed={true} - fullWidth={false} - incremental={false} - isClearable={true} - isInvalid={false} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - placeholder="Service name" - prepend="Focus on" - value="" - /> + + + } + aria-controls="service-select-dropdown" + compressed={true} + disabled={false} + fullWidth={false} + incremental={false} + isClearable={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + placeholder="Service name" + prepend="Focus on" + value="" + /> + } closePopover={[Function]} display="inlineBlock" @@ -2396,169 +2438,195 @@ exports[`Service Config component renders with one service selected 1`] = `
- - } - aria-controls="service-select-dropdown" - compressed={true} - fullWidth={false} - incremental={false} - isClearable={true} - isInvalid={false} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - placeholder="Service name" - prepend="Focus on" - value="" + - - } - compressed={true} - fullWidth={false} - icon="search" - isLoading={false} - prepend="Focus on" + -
+ } + aria-controls="service-select-dropdown" + compressed={true} + disabled={false} + fullWidth={false} + incremental={false} + isClearable={true} + isInvalid={false} + isLoading={false} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onFocus={[Function]} + placeholder="Service name" + prepend="Focus on" + value="" > - - - -
- - - - +
+ + +
- - + + +
- - - + + + + + + + + +
+
- -
- - - -
- - + + + + + + + + +
+
+
+ +
diff --git a/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map.test.tsx.snap b/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map.test.tsx.snap index 390230fd8..77d18ceea 100644 --- a/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map.test.tsx.snap +++ b/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map.test.tsx.snap @@ -1,112 +1,249 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Service map component renders service map 1`] = ` - - - - - - +
+ + Service map + +
+
- - + - - } - aria-controls="service-select-dropdown" - compressed={true} - fullWidth={false} - incremental={false} - isClearable={true} - isInvalid={false} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - placeholder="Service name" - prepend="Focus on" - value="" - /> + Select metric for service map display + +
+ + + +
+ +
+
+
+
+
- - - - - - - +
+ +
+ +
+ + + + + +
+
+ +
+ +
+
+
+
+
+
- +
+
+ Mock Graph +
-
- - +
- - - - - - + > +
+
+
+
+ unmatched-node-legend +
+
+
+ No match +
+
+
+
+
+
+
, +
, +] `; diff --git a/public/components/trace_analytics/components/common/plots/__tests__/service_map.test.tsx b/public/components/trace_analytics/components/common/plots/__tests__/service_map.test.tsx index b7d505fb8..9b77009b2 100644 --- a/public/components/trace_analytics/components/common/plots/__tests__/service_map.test.tsx +++ b/public/components/trace_analytics/components/common/plots/__tests__/service_map.test.tsx @@ -3,25 +3,202 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { configure, shallow } from 'enzyme'; +import { configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; +import { mount } from 'enzyme'; import React from 'react'; -import { TEST_SERVICE_MAP } from '../../../../../../../test/constants'; +import { act } from '@testing-library/react'; import { ServiceMap } from '../service_map'; +import { EuiFieldSearch, EuiSelectable } from '@elastic/eui'; +import { TEST_SERVICE_MAP, MOCK_CANVAS_CONTEXT } from '../../../../../../../test/constants'; +import Graph from 'react-graph-vis'; +import toJson from 'enzyme-to-json'; -describe('Service map component', () => { - configure({ adapter: new Adapter() }); - - it('renders service map', async () => { - const setServiceMapIdSelected = jest.fn((e) => {}); - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); +configure({ adapter: new Adapter() }); + +// Mock uuid +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'static-uuid'), +})); + +// Normalize dynamic values in snapshots +expect.addSnapshotSerializer({ + test: (val) => typeof val === 'string' && /^[a-f0-9-]{36}$/.test(val), + print: () => '""', +}); + +// Mock crypto.getRandomValues +const crypto = { + getRandomValues: jest.fn((arr) => arr.fill(0)), // Fill with consistent values +}; +Object.defineProperty(global, 'crypto', { value: crypto }); + +jest + .spyOn(HTMLCanvasElement.prototype, 'getContext') + .mockImplementation((contextId) => + contextId === '2d' ? ((MOCK_CANVAS_CONTEXT as unknown) as CanvasRenderingContext2D) : null + ); + +jest.mock('react-graph-vis', () => { + const GraphMock = () =>
Mock Graph
; + return GraphMock; +}); + +async function setFocusOnService(wrapper: ReturnType, serviceName: string) { + const searchField = wrapper.find(EuiFieldSearch).first(); + expect(searchField.exists()).toBeTruthy(); + + await act(async () => { + const mockEvent = { + preventDefault: jest.fn(), + stopPropagation: jest.fn(), + target: { value: '' }, + }; + searchField.prop('onClick')?.((mockEvent as unknown) as React.MouseEvent); + }); + wrapper.update(); + + const selectable = wrapper.find(EuiSelectable); + const onChange = selectable.prop('onChange'); + + if (onChange) { + await act(async () => { + onChange([{ label: serviceName, checked: 'on' }]); + }); + wrapper.update(); + } else { + throw new Error('onChange handler is undefined on EuiSelectable'); + } + expect(wrapper.find(EuiFieldSearch).prop('placeholder')).toBe(serviceName); +} + +describe('ServiceMap Component', () => { + const defaultProps = { + serviceMap: TEST_SERVICE_MAP, + idSelected: 'latency' as 'latency' | 'error_rate' | 'throughput', + setIdSelected: jest.fn(), + page: 'dashboard' as + | 'app' + | 'appCreate' + | 'dashboard' + | 'traces' + | 'services' + | 'serviceView' + | 'detailFlyout' + | 'traceView', + mode: 'jaeger', + currService: '', + filters: [], + setFilters: jest.fn(), + addFilter: jest.fn(), + removeFilter: jest.fn(), + isLoading: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders service map component', async () => { + const wrapper = mount(); + wrapper.update(); + + await act(async () => { + expect( + toJson(wrapper, { + noKey: false, + mode: 'deep', + }) + ).toMatchSnapshot(); + }); + }); + + it('renders application composition map title when page is app', () => { + const wrapper = mount(); + expect(wrapper.find('PanelTitle').prop('title')).toBe('Application Composition Map'); + }); + + it('renders service map title for other pages', () => { + const wrapper = mount(); + expect(wrapper.find('PanelTitle').prop('title')).toBe('Service map'); + }); + + describe('Service search and selection', () => { + it('updates placeholder when service is focused', async () => { + const wrapper = mount(); + await setFocusOnService(wrapper, 'order'); + }); + + it('clears focus with refresh button', async () => { + const wrapper = mount(); + await setFocusOnService(wrapper, 'order'); + + // Verify focus is set + expect(wrapper.find(EuiFieldSearch).prop('placeholder')).toBe('order'); + + // Find and click the refresh button + const refreshButton = wrapper.find('button[data-test-subj="serviceMapRefreshButton"]'); + expect(refreshButton.exists()).toBeTruthy(); + + await act(async () => { + refreshButton.simulate('click'); + }); + wrapper.update(); + + // Verify the search field is cleared + const updatedSearchField = wrapper.find(EuiFieldSearch).first(); + expect(updatedSearchField.prop('value')).toBe(''); + expect(updatedSearchField.prop('placeholder')).not.toBe('order'); + }); + }); + + describe('Metric selection', () => { + it('changes selected metric', async () => { + const setIdSelected = jest.fn(); + const wrapper = mount(); + + const buttonGroup = wrapper.find('EuiButtonGroup'); + const onChange = buttonGroup.prop('onChange'); + + if (onChange) { + await act(async () => { + onChange('error_rate' as any); + }); + wrapper.update(); + } else { + throw new Error('onChange handler is undefined on EuiButtonGroup'); + } + + expect(setIdSelected).toHaveBeenCalledWith('error_rate'); + }); + }); + + describe('Service dependencies', () => { + it('shows related services when focusing on a service', async () => { + const wrapper = mount(); + await setFocusOnService(wrapper, 'order'); + + // Verify that the graph exists and has nodes + const graph = wrapper.find(Graph); + expect(graph.exists()).toBeTruthy(); + + const graphProps = graph.props() as { graph: { nodes: any[] } }; + expect(graphProps.graph).toBeDefined(); + expect(graphProps.graph.nodes.length).toBeGreaterThan(0); + }); + }); + + describe('Loading state', () => { + it('shows loading indicator when isLoading is true', () => { + const wrapper = mount(); + expect(wrapper.find('.euiLoadingSpinner').exists()).toBeTruthy(); + }); + }); + + describe('Empty state', () => { + it('handles empty service map', () => { + const wrapper = mount(); + expect(wrapper.find('Graph').exists()).toBeFalsy(); + }); }); }); diff --git a/public/components/trace_analytics/components/common/plots/service_map.tsx b/public/components/trace_analytics/components/common/plots/service_map.tsx index b764e21f6..ae53fb5f8 100644 --- a/public/components/trace_analytics/components/common/plots/service_map.tsx +++ b/public/components/trace_analytics/components/common/plots/service_map.tsx @@ -6,20 +6,20 @@ import { EuiButtonGroup, EuiButtonIcon, + EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, + EuiLoadingSpinner, EuiPanel, + EuiPopover, + EuiSelectable, EuiSpacer, EuiSuperSelect, EuiSuperSelectOption, - EuiSelectable, - EuiSelectableOption, - EuiPopover, - EuiFieldSearch, - EuiLoadingSpinner, + EuiToolTip, } from '@elastic/eui'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; // @ts-ignore import Graph from 'react-graph-vis'; import { ServiceNodeDetails } from '../../../../../../common/types/trace_analytics'; @@ -61,6 +61,8 @@ export function ServiceMap({ filterByCurrService, includeMetricsCallback, mode, + filters = [], + setFilters, hideSearchBar = false, }: { serviceMap: ServiceObject; @@ -81,6 +83,8 @@ export function ServiceMap({ filterByCurrService?: boolean; includeMetricsCallback?: () => void; mode?: string; + filters: FilterType[]; + setFilters: (filters: FilterType[]) => void; hideSearchBar?: boolean; }) { const [graphKey, setGraphKey] = useState(0); // adding key to allow for re-renders @@ -89,11 +93,9 @@ export function ServiceMap({ const [ticks, setTicks] = useState([]); const [items, setItems] = useState({}); const [query, setQuery] = useState(''); - const [selectableOptions, setSelectableOptions] = useState([]); const [isLoading, setIsLoading] = useState(true); const [filterChange, setIsFilterChange] = useState(false); const [focusedService, setFocusedService] = useState(null); - const [clearFilterRequest, setClearFilterRequest] = useState(false); const toggleButtons = [ { @@ -111,10 +113,16 @@ export function ServiceMap({ ]; const [selectedNodeDetails, setSelectedNodeDetails] = useState(null); - const [selectableValue, setSelectableValue] = useState>>([]); const [isPopoverOpen, setPopoverOpen] = useState(false); + // Memoize a boolean to determine if the focus bar should be disabled + const isFocusBarDisabled = useMemo(() => { + return filters.some( + (filter) => filter.field === 'serviceName' && focusedService === filter.value + ); + }, [filters, focusedService]); + const onChangeSelectable = (value: React.SetStateAction>>) => { // if the change is changing for the first time then callback servicemap with metrics if (selectableValue.length === 0 && value.length !== 0) { @@ -141,59 +149,14 @@ export function ServiceMap({ }, ]; - const clearFilter = () => { - setFocusedService(null); - setClearFilterRequest(true); + const removeFilter = (field: string, value: string) => { + if (!setFilters) return; + const updatedFilters = filters.filter( + (filter) => !(filter.field === field && filter.value === value) + ); + setFilters(updatedFilters); }; - useEffect(() => { - if (clearFilterRequest && focusedService === null) { - setClearFilterRequest(false); - - setQuery(''); - currService = ''; - - if (addFilter) { - addFilter({ - field: 'serviceName', - operator: 'is', - value: '', - inverted: false, - disabled: true, // Disable the filter to effectively clear it - }); - } - - // Reset the graph to show the full view - setItems( - getServiceMapGraph( - serviceMap, - idSelected, - ticks, - undefined, - serviceMap[currService!]?.relatedServices, - false // Do not filter by the current service to show the entire graph - ) - ); - - setInvalid(false); - } - }, [focusedService, clearFilterRequest]); - - useEffect(() => { - if (items?.graph?.nodes) { - const visibleNodes = items.graph.nodes.map((node) => node.label); - const options = Object.keys(serviceMap) - .filter((key) => visibleNodes.includes(serviceMap[key].serviceName)) - .map((key) => ({ - label: serviceMap[key].serviceName, - value: serviceMap[key].serviceName, - })); - setSelectableOptions(options); - } else { - setSelectableOptions([]); // Ensure options are empty if items.graph.nodes doesn't exist - } - }, [items.graph, serviceMap]); - const options = { layout: { randomSeed: 10, @@ -267,8 +230,6 @@ export function ServiceMap({ }; const addServiceFilter = (selectedServiceName) => { - if (selectedServiceName === focusedService) return; - if (!addFilter) return; if (selectedServiceName) { @@ -328,33 +289,36 @@ export function ServiceMap({ }; const onFocus = (service: string) => { - if (service.length === 0) { - clearFilter(); + if (!service) { + // Clear focus if no service is provided + if (focusedService !== null) { + removeFilter('serviceName', focusedService); + setItems( + getServiceMapGraph( + serviceMap, + idSelected, + ticks, + undefined, + undefined, + false // Show the entire graph without filtering + ) + ); + setFocusedService(null); + setInvalid(false); + } } else if (serviceMap[service]) { - // Focus on the specified service and add a filter - setFocusedService(service); - if (addFilter) { - addFilter({ - field: 'serviceName', - operator: 'is', - value: service, - inverted: false, - disabled: false, - }); + if (focusedService !== service) { + const filteredGraph = getServiceMapGraph( + serviceMap, + idSelected, + ticks, + service, + serviceMap[service]?.relatedServices, + true // Enable filtering to focus on connected nodes + ); + setItems(filteredGraph); + setFocusedService(service); } - - const filteredGraph = getServiceMapGraph( - serviceMap, - idSelected, - ticks, - service, - serviceMap[service]?.relatedServices, - true // Enable filtering by the current service to show only connected nodes - ); - setItems(filteredGraph); - setInvalid(false); - } else { - setInvalid(true); } }; @@ -388,10 +352,6 @@ export function ServiceMap({ }, [items]); useEffect(() => { - if (currService === focusedService) { - return; - } - if (!serviceMap || Object.keys(serviceMap).length === 0) { setItems({}); return; @@ -404,17 +364,19 @@ export function ServiceMap({ const max = Math.max(...values); const calculatedTicks = calculateTicks(min, max); setTicks(calculatedTicks); + // Adjust graph rendering logic to ensure related services are visible + const showRelatedServices = focusedService ? true : filterByCurrService; setItems( getServiceMapGraph( serviceMap, idSelected, calculatedTicks, - currService, + focusedService ?? currService, serviceMap[currService!]?.relatedServices, - filterByCurrService + showRelatedServices ) ); - }, [serviceMap, idSelected]); + }, [serviceMap, idSelected, focusedService, filterByCurrService]); return ( <> @@ -443,35 +405,53 @@ export function ServiceMap({ setPopoverOpen(!isPopoverOpen)} - onChange={(e) => { - const newValue = e.target.value; - setQuery(newValue); - if (newValue === '') { - setGraphKey((prevKey) => prevKey + 1); - setQuery(''); - onFocus(''); - } - }} - isInvalid={query.length > 0 && invalid} - append={ - { - setGraphKey((prevKey) => prevKey + 1); - setQuery(''); - onFocus(''); - }} - /> + + > + { + if (!isFocusBarDisabled) setPopoverOpen(!isPopoverOpen); + }} + onChange={(e) => { + if (!isFocusBarDisabled) { + const newValue = e.target.value; + setQuery(newValue); + if (newValue === '') { + setGraphKey((prevKey) => prevKey + 1); + setQuery(''); + onFocus(focusedService || ''); + } + } + }} + isInvalid={query.length > 0 && invalid} + append={ + { + if (!isFocusBarDisabled) { + setGraphKey((prevKey) => prevKey + 1); + } + setQuery(''); + onFocus(''); + }} + /> + } + aria-controls="service-select-dropdown" + disabled={isFocusBarDisabled} + /> + } isOpen={isPopoverOpen} closePopover={() => setPopoverOpen(false)} @@ -489,9 +469,14 @@ export function ServiceMap({ isClearable: true, autoFocus: true, }} - options={selectableOptions.filter((option) => - option.label.toLowerCase().includes(query.toLowerCase()) - )} + options={ + items?.graph?.nodes + ?.filter((node) => node.label.toLowerCase().includes(query.toLowerCase())) + .map((node) => ({ + label: node.label, + checked: focusedService === node.label ? 'on' : undefined, + })) || [] + } singleSelection={true} onChange={(newOptions) => { const selectedOption = newOptions.find((option) => option.checked === 'on'); @@ -500,9 +485,10 @@ export function ServiceMap({ setPopoverOpen(false); return; } - setQuery(selectedOption.label); + setQuery(''); onFocus(selectedOption.label); setPopoverOpen(false); + setGraphKey((prevKey) => prevKey + 1); } }} listProps={{ bordered: true, style: { width: '300px' } }} @@ -539,7 +525,7 @@ export function ServiceMap({ getNetwork={(networkInstance: any) => { setNetwork(networkInstance); setZoomLimits(networkInstance); - if (currService) onFocus(currService, networkInstance); + if (currService) onFocus(currService); }} /> )} @@ -554,11 +540,11 @@ export function ServiceMap({ display: 'flex', alignItems: 'center', justifyContent: 'center', - backgroundColor: 'rgba(255, 255, 255, 0.8)', + backgroundColor: 'transparent', // supports both dark and light themes zIndex: 1000, }} > - +
)} {selectedNodeDetails && ( diff --git a/public/components/trace_analytics/components/services/__tests__/__snapshots__/service_view.test.tsx.snap b/public/components/trace_analytics/components/services/__tests__/__snapshots__/service_view.test.tsx.snap index 6a7701e0a..91adde4f7 100644 --- a/public/components/trace_analytics/components/services/__tests__/__snapshots__/service_view.test.tsx.snap +++ b/public/components/trace_analytics/components/services/__tests__/__snapshots__/service_view.test.tsx.snap @@ -232,7 +232,7 @@ exports[`Service view component renders service view 1`] = ` @@ -2057,27 +2082,35 @@ exports[`Services component renders empty services page 1`] = ` - } - aria-controls="service-select-dropdown" - compressed={true} - fullWidth={false} - incremental={false} - isClearable={true} - isInvalid={false} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - placeholder="Service name" - prepend="Focus on" - value="" - /> + + + } + aria-controls="service-select-dropdown" + compressed={true} + disabled={false} + fullWidth={false} + incremental={false} + isClearable={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + placeholder="Service name" + prepend="Focus on" + value="" + /> + } closePopover={[Function]} display="inlineBlock" @@ -2103,169 +2136,195 @@ exports[`Services component renders empty services page 1`] = `
- - } - aria-controls="service-select-dropdown" - compressed={true} - fullWidth={false} - incremental={false} - isClearable={true} - isInvalid={false} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - placeholder="Service name" - prepend="Focus on" - value="" + - - } - compressed={true} - fullWidth={false} - icon="search" - isLoading={false} - prepend="Focus on" + -
+ } + aria-controls="service-select-dropdown" + compressed={true} + disabled={false} + fullWidth={false} + incremental={false} + isClearable={true} + isInvalid={false} + isLoading={false} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onFocus={[Function]} + placeholder="Service name" + prepend="Focus on" + value="" > - - - -
- - - - +
+ + +
- + + + - - - - + + + + + + + + +
+
-
-
- - - -
-
-
+ + + + + + + + +
+ + + +
@@ -3839,11 +3898,20 @@ exports[`Services component renders jaeger services page 1`] = ` addFilter={[Function]} addServicesGroupFilter={[Function]} dataPrepperIndicesExist={false} + dataSourceMDSId={ + Array [ + Object { + "id": "", + "label": "", + }, + ] + } isServiceTrendEnabled={false} items={Array []} jaegerIndicesExist={true} loading={true} mode="jaeger" + page="services" selectedItems={Array []} serviceTrends={Object {}} setIsServiceTrendEnabled={[Function]} @@ -5530,10 +5598,19 @@ exports[`Services component renders services page 1`] = ` addFilter={[Function]} addServicesGroupFilter={[Function]} dataPrepperIndicesExist={true} + dataSourceMDSId={ + Array [ + Object { + "id": "", + "label": "", + }, + ] + } isServiceTrendEnabled={false} items={Array []} loading={true} mode="data_prepper" + page="services" selectedItems={Array []} serviceTrends={Object {}} setIsServiceTrendEnabled={[Function]} @@ -5797,10 +5874,26 @@ exports[`Services component renders services page 1`] = ` @@ -6134,27 +6227,35 @@ exports[`Services component renders services page 1`] = ` - } - aria-controls="service-select-dropdown" - compressed={true} - fullWidth={false} - incremental={false} - isClearable={true} - isInvalid={false} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - placeholder="Service name" - prepend="Focus on" - value="" - /> + + + } + aria-controls="service-select-dropdown" + compressed={true} + disabled={false} + fullWidth={false} + incremental={false} + isClearable={true} + isInvalid={false} + isLoading={false} + onChange={[Function]} + onClick={[Function]} + placeholder="Service name" + prepend="Focus on" + value="" + /> + } closePopover={[Function]} display="inlineBlock" @@ -6180,169 +6281,195 @@ exports[`Services component renders services page 1`] = `
- - } - aria-controls="service-select-dropdown" - compressed={true} - fullWidth={false} - incremental={false} - isClearable={true} - isInvalid={false} - isLoading={false} - onChange={[Function]} - onClick={[Function]} - placeholder="Service name" - prepend="Focus on" - value="" + - - } - compressed={true} - fullWidth={false} - icon="search" - isLoading={false} - prepend="Focus on" + -
+ } + aria-controls="service-select-dropdown" + compressed={true} + disabled={false} + fullWidth={false} + incremental={false} + isClearable={true} + isInvalid={false} + isLoading={false} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onFocus={[Function]} + placeholder="Service name" + prepend="Focus on" + value="" > - - - -
- - - - +
+ + +
- - + + +
- - - + + + + + + + + +
+
- -
- - - -
- - + + + + + + + + +
+
+
+ +
diff --git a/public/components/trace_analytics/components/services/__tests__/__snapshots__/services_table.test.tsx.snap b/public/components/trace_analytics/components/services/__tests__/__snapshots__/services_table.test.tsx.snap index 91c056f4f..af114a4cf 100644 --- a/public/components/trace_analytics/components/services/__tests__/__snapshots__/services_table.test.tsx.snap +++ b/public/components/trace_analytics/components/services/__tests__/__snapshots__/services_table.test.tsx.snap @@ -670,6 +670,7 @@ exports[`Services table component renders jaeger services table 1`] = ` "field": "actions", "name": "Actions", "render": [Function], + "sortable": false, }, ] } @@ -758,6 +759,7 @@ exports[`Services table component renders jaeger services table 1`] = ` "field": "actions", "name": "Actions", "render": [Function], + "sortable": false, }, ] } @@ -2672,6 +2674,7 @@ exports[`Services table component renders services table 1`] = ` "field": "actions", "name": "Actions", "render": [Function], + "sortable": false, }, ] } @@ -2782,6 +2785,7 @@ exports[`Services table component renders services table 1`] = ` "field": "actions", "name": "Actions", "render": [Function], + "sortable": false, }, ] } diff --git a/public/components/trace_analytics/components/services/__tests__/service_view.test.tsx b/public/components/trace_analytics/components/services/__tests__/service_view.test.tsx index 0299fe255..a269be81e 100644 --- a/public/components/trace_analytics/components/services/__tests__/service_view.test.tsx +++ b/public/components/trace_analytics/components/services/__tests__/service_view.test.tsx @@ -7,39 +7,51 @@ import { configure, shallow } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; import { ServiceView } from '..'; -import { coreStartMock } from '../../../../../../test/__mocks__/coreMocks'; + +jest.mock('../../../../../../test/__mocks__/coreMocks', () => ({ + coreStartMock: { + chrome: { setBreadcrumbs: jest.fn() }, + http: { post: jest.fn() }, + }, +})); + +jest.mock('react-router-dom', () => ({ + useLocation: jest.fn().mockReturnValue({ + pathname: '/services', + search: '?serviceId=test-id', + hash: '', + state: null, + key: '', + }), + useHistory: jest.fn(), +})); describe('Service view component', () => { configure({ adapter: new Adapter() }); - it('renders service view', () => { - const core = coreStartMock; - const setQuery = jest.fn(); - const setFilters = jest.fn(); - const setStartTime = jest.fn(); - const setEndTime = jest.fn(); - const addFilter = jest.fn(); - const wrapper = shallow( - - ); + const { coreStartMock } = jest.requireMock('../../../../../../test/__mocks__/coreMocks'); + const defaultProps = { + serviceName: 'order', + chrome: coreStartMock.chrome, + appConfigs: [], + parentBreadcrumbs: [{ text: 'test', href: 'test#/' }], + http: coreStartMock.http, + query: '', + setQuery: jest.fn(), + filters: [], + setFilters: jest.fn(), + startTime: 'now-5m', + setStartTime: jest.fn(), + endTime: 'now', + setEndTime: jest.fn(), + addFilter: jest.fn(), + mode: 'data_prepper', + dataSourceMDSId: [{ id: '', label: '' }], + }; + + it('renders service view', () => { + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/public/components/trace_analytics/components/services/__tests__/services_table.test.tsx b/public/components/trace_analytics/components/services/__tests__/services_table.test.tsx index d65bcb6b1..b35fe81c3 100644 --- a/public/components/trace_analytics/components/services/__tests__/services_table.test.tsx +++ b/public/components/trace_analytics/components/services/__tests__/services_table.test.tsx @@ -7,6 +7,7 @@ import { configure, mount } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import React from 'react'; import { ServicesTable } from '../services_table'; +import { generateServiceUrl } from '../../common/helper_functions'; describe('Services table component', () => { configure({ adapter: new Adapter() }); @@ -114,4 +115,55 @@ describe('Services table component', () => { expect(wrapper).toMatchSnapshot(); }); + + it('redirects to the correct URL when the service link is clicked', () => { + const mockDataSourceId = 'mock-data-source-id'; + const tableItems = [ + { + name: 'checkoutservice', + average_latency: 100, + error_rate: 0.5, + throughput: 200, + traces: 10, + itemId: '1', + }, + ]; + + // Mock window.location before rendering + const originalLocation = window.location; + delete window.location; + window.location = { ...originalLocation }; + + const wrapper = mount( + + ); + + // Find and click the service link + const serviceLink = wrapper.find('[data-test-subj="service-link"]').first(); + expect(serviceLink.exists()).toBeTruthy(); + + serviceLink.simulate('click'); + + const expectedUrl = generateServiceUrl('checkoutservice', mockDataSourceId); + expect(window.location.href).toBe(expectedUrl); + + window.location = originalLocation; + }); }); diff --git a/public/components/trace_analytics/components/services/service_view.tsx b/public/components/trace_analytics/components/services/service_view.tsx index ed00bc193..f32772cfb 100644 --- a/public/components/trace_analytics/components/services/service_view.tsx +++ b/public/components/trace_analytics/components/services/service_view.tsx @@ -27,6 +27,7 @@ import { } from '@elastic/eui'; import _ from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; import { DataSourceManagementPluginSetup } from '../../../../../../../src/plugins/data_source_management/public'; import { DataSourceOption } from '../../../../../../../src/plugins/data_source_management/public/components/data_source_menu/types'; import { @@ -75,6 +76,20 @@ export function ServiceView(props: ServiceViewProps) { >('latency'); const [redirect, setRedirect] = useState(false); const [actionsMenuPopover, setActionsMenuPopover] = useState(false); + const [serviceId, setServiceId] = useState(null); + const location = useLocation(); + + useEffect(() => { + try { + const params = new URLSearchParams(location?.search || ''); + const id = params.get('serviceId'); + setServiceId(id); + } catch (error) { + setServiceId(null); + } + }, [location]); + + const hideSearchBarCheck = page === 'serviceFlyout' || serviceId !== ''; const refresh = () => { const DSL = filtersToDsl( @@ -535,7 +550,7 @@ export function ServiceView(props: ServiceViewProps) { page="serviceView" filterByCurrService={true} mode={mode} - hideSearchBar={page === 'serviceFlyout'} + hideSearchBar={hideSearchBarCheck} /> ) : ( diff --git a/public/components/trace_analytics/components/services/services_content.tsx b/public/components/trace_analytics/components/services/services_content.tsx index 4535eeda2..d78aa5747 100644 --- a/public/components/trace_analytics/components/services/services_content.tsx +++ b/public/components/trace_analytics/components/services/services_content.tsx @@ -233,12 +233,16 @@ export function ServicesContent(props: ServicesProps) { isServiceTrendEnabled={isServiceTrendEnabled} setIsServiceTrendEnabled={setIsServiceTrendEnabled} serviceTrends={serviceTrends} + dataSourceMDSId={props.dataSourceMDSId} + page={page} /> {mode === 'custom_data_prepper' || (mode === 'data_prepper' && dataPrepperIndicesExist) ? ( >; serviceTrends: ServiceTrends; + dataSourceMDSId: DataSourceOption[]; + page: 'app' | 'services'; } export function ServicesTable(props: ServicesTableProps) { @@ -65,6 +69,8 @@ export function ServicesTable(props: ServicesTableProps) { isServiceTrendEnabled, setIsServiceTrendEnabled, serviceTrends, + dataSourceMDSId, + page, } = props; const selectionValue = { @@ -72,13 +78,11 @@ export function ServicesTable(props: ServicesTableProps) { }; const nameColumnAction = (serviceName: string) => { - addFilter({ - field: mode === 'jaeger' ? 'process.serviceName' : 'serviceName', - operator: 'is', - value: serviceName, - inverted: false, - disabled: false, - }); + if (page === 'app') { + setCurrentSelectedService(serviceName); + } else { + window.location.href = generateServiceUrl(serviceName, dataSourceMDSId[0].id); + } }; const renderTitleBar = (totalItems?: number) => { @@ -116,144 +120,148 @@ export function ServicesTable(props: ServicesTableProps) { ); }; - const columns = useMemo( - () => - [ - { - field: 'name', - name: 'Name', - align: 'left', - sortable: true, - render: (item: any) => ( - nameColumnAction(item)}> - {item.length < 24 ? item :
{truncate(item, { length: 24 })}
} -
- ), - }, - { - field: 'average_latency', - name: 'Average duration (ms)', - align: 'right', - sortable: true, - render: (item: any, row: any) => ( - - ), - }, - { - field: 'error_rate', - name: 'Error rate', - align: 'right', - sortable: true, - render: (item: any, row: any) => ( - - ), - }, - { - field: 'throughput', - name: 'Request rate', - align: 'right', - sortable: true, - truncateText: true, - render: (item: any, row: any) => ( - - ), - }, - ...(mode === 'data_prepper' || mode === 'custom_data_prepper' - ? [ - { - field: 'number_of_connected_services', - name: 'No. of connected services', - align: 'right', - sortable: true, - truncateText: true, - width: '80px', - render: (item: any) => (item === 0 || item ? item : '-'), - }, - ] - : []), - ...(mode === 'data_prepper' || mode === 'custom_data_prepper' - ? [ - { - field: 'connected_services', - name: 'Connected services', - align: 'left', - sortable: true, - truncateText: true, - render: (item: any) => - item ? ( - {truncate(item.join(', '), { length: 50 })} - ) : ( - '-' - ), - }, - ] - : []), + const columns = useMemo(() => { + const baseColumns = [ + { + field: 'name', + name: 'Name', + align: 'left', + sortable: true, + render: (item: any) => ( + nameColumnAction(item)}> + {item.length < 24 ? item :
{truncate(item, { length: 24 })}
} +
+ ), + }, + { + field: 'average_latency', + name: 'Average duration (ms)', + align: 'right', + sortable: true, + render: (item: any, row: any) => ( + + ), + }, + { + field: 'error_rate', + name: 'Error rate', + align: 'right', + sortable: true, + render: (item: any, row: any) => ( + + ), + }, + { + field: 'throughput', + name: 'Request rate', + align: 'right', + sortable: true, + truncateText: true, + render: (item: any, row: any) => ( + + ), + }, + ...(mode === 'data_prepper' || mode === 'custom_data_prepper' + ? [ + { + field: 'number_of_connected_services', + name: 'No. of connected services', + align: 'right', + sortable: true, + truncateText: true, + width: '80px', + render: (item: any) => (item === 0 || item ? item : '-'), + }, + ] + : []), + ...(mode === 'data_prepper' || mode === 'custom_data_prepper' + ? [ + { + field: 'connected_services', + name: 'Connected services', + align: 'left', + sortable: true, + truncateText: true, + render: (item: any) => + item ? ( + {truncate(item.join(', '), { length: 50 })} + ) : ( + '-' + ), + }, + ] + : []), - { - field: 'traces', - name: 'Traces', - align: 'right', - sortable: true, - truncateText: true, - render: (item: any, row: any) => ( - <> - {item === 0 || item ? ( - { - setRedirect(true); - addFilter({ - field: mode === 'jaeger' ? 'process.serviceName' : 'serviceName', - operator: 'is', - value: row.name, - inverted: false, - disabled: false, - }); - traceColumnAction(); - }} - > - - - ) : ( - '-' - )} - - ), - }, - { - field: 'actions', - name: 'Actions', - align: 'center', - render: (_item: any, row: any) => ( - - setCurrentSelectedService(row.name)}> - - - - - - ), - }, - ] as Array>, - [items] - ); + { + field: 'traces', + name: 'Traces', + align: 'right', + sortable: true, + truncateText: true, + render: (item: any, row: any) => ( + <> + {item === 0 || item ? ( + { + setRedirect(true); + addFilter({ + field: mode === 'jaeger' ? 'process.serviceName' : 'serviceName', + operator: 'is', + value: row.name, + inverted: false, + disabled: false, + }); + traceColumnAction(); + }} + > + + + ) : ( + '-' + )} + + ), + }, + ]; + + if (page !== 'app') { + baseColumns.push({ + field: 'actions', + name: 'Actions', + align: 'center', + render: (_item: any, row: any) => ( + + setCurrentSelectedService(row.name)}> + + + + + + ), + sortable: false, + }); + } + + return baseColumns as Array>; + }, [items, page]); const titleBar = useMemo(() => renderTitleBar(items?.length), [ items, diff --git a/public/components/trace_analytics/components/traces/span_detail_panel.tsx b/public/components/trace_analytics/components/traces/span_detail_panel.tsx index 09ad0b480..12ae94359 100644 --- a/public/components/trace_analytics/components/traces/span_detail_panel.tsx +++ b/public/components/trace_analytics/components/traces/span_detail_panel.tsx @@ -226,7 +226,9 @@ export function SpanDetailPanel(props: { plot_bgcolor: 'rgba(0, 0, 0, 0)', paper_bgcolor: 'rgba(0, 0, 0, 0)', height: 25 * plotTraces.length + 60, - width: props.isApplicationFlyout ? undefined : availableWidth - dynamicWidthAdjustment, // Allow plotly to render the gantt chart full screen with padding + width: props.isApplicationFlyout + ? availableWidth / 2 - 100 // Allow gantt chart to fit in flyout + : availableWidth - dynamicWidthAdjustment, // Allow gantt chart to render full screen margin: { l: dynamicLeftMargin, r: 5, diff --git a/public/components/trace_analytics/home.tsx b/public/components/trace_analytics/home.tsx index d107bfe2d..ccdb45c39 100644 --- a/public/components/trace_analytics/home.tsx +++ b/public/components/trace_analytics/home.tsx @@ -490,9 +490,9 @@ export const Home = (props: HomeProps) => { }} /> } /> + {flyout} + {spanFlyoutComponent} - {flyout} - {spanFlyoutComponent} ); }; diff --git a/test/constants.ts b/test/constants.ts index fe399e08b..98ad23ca0 100644 --- a/test/constants.ts +++ b/test/constants.ts @@ -419,6 +419,7 @@ export const TEST_SERVICE_MAP = { order: { serviceName: 'order', id: 1, + average_latency: 100, traceGroups: [ { traceGroup: 'client_cancel_order', @@ -442,10 +443,12 @@ export const TEST_SERVICE_MAP = { latency: 90.1, error_rate: 4.17, throughput: 48, + relatedServices: ['analytics-service', 'database', 'frontend-client'], }, 'analytics-service': { serviceName: 'analytics-service', id: 2, + average_latency: 150, traceGroups: [ { traceGroup: 'client_cancel_order', @@ -477,10 +480,12 @@ export const TEST_SERVICE_MAP = { latency: 12.99, error_rate: 0, throughput: 37, + relatedServices: ['order', 'inventory', 'authentication', 'payment', 'recommendation'], }, database: { serviceName: 'database', id: 3, + average_latency: 200, traceGroups: [ { traceGroup: 'client_cancel_order', @@ -504,7 +509,7 @@ export const TEST_SERVICE_MAP = { }, { traceGroup: 'load_main_screen', - targetResource: ['getIntentory'], + targetResource: ['getInventory'], }, ], targetServices: [], @@ -512,10 +517,12 @@ export const TEST_SERVICE_MAP = { latency: 49.54, error_rate: 3.77, throughput: 53, + relatedServices: ['order', 'inventory'], }, 'frontend-client': { serviceName: 'frontend-client', id: 4, + average_latency: 250, traceGroups: [ { traceGroup: 'client_cancel_order', @@ -547,10 +554,12 @@ export const TEST_SERVICE_MAP = { latency: 207.71, error_rate: 7.41, throughput: 27, + relatedServices: ['order', 'payment', 'authentication'], }, inventory: { serviceName: 'inventory', id: 5, + average_latency: 300, traceGroups: [ { traceGroup: 'client_checkout', @@ -566,10 +575,12 @@ export const TEST_SERVICE_MAP = { latency: 183.52, error_rate: 3.23, throughput: 31, + relatedServices: ['analytics-service', 'database', 'payment', 'recommendation'], }, authentication: { serviceName: 'authentication', id: 6, + average_latency: 350, traceGroups: [ { traceGroup: 'load_main_screen', @@ -581,10 +592,12 @@ export const TEST_SERVICE_MAP = { latency: 139.09, error_rate: 8.33, throughput: 12, + relatedServices: ['analytics-service', 'recommendation', 'frontend-client'], }, payment: { serviceName: 'payment', id: 7, + average_latency: 400, traceGroups: [ { traceGroup: 'client_checkout', @@ -596,10 +609,12 @@ export const TEST_SERVICE_MAP = { latency: 134.36, error_rate: 9.09, throughput: 11, + relatedServices: ['analytics-service', 'inventory', 'frontend-client'], }, recommendation: { serviceName: 'recommendation', id: 8, + average_latency: 450, traceGroups: [ { traceGroup: 'load_main_screen', @@ -611,6 +626,7 @@ export const TEST_SERVICE_MAP = { latency: 176.97, error_rate: 6.25, throughput: 16, + relatedServices: ['analytics-service', 'inventory', 'authentication'], }, }; @@ -769,3 +785,56 @@ export const fieldCapQueryResponse2 = { }, }, }; + +export const MOCK_CANVAS_CONTEXT = { + canvas: document.createElement('canvas'), + fillRect: jest.fn(), + clearRect: jest.fn(), + getImageData: jest.fn(() => ({ data: new Uint8ClampedArray() })), + putImageData: jest.fn(), + createImageData: jest.fn(), + setTransform: jest.fn(), + drawImage: jest.fn(), + save: jest.fn(), + fillText: jest.fn(), + restore: jest.fn(), + beginPath: jest.fn(), + moveTo: jest.fn(), + lineTo: jest.fn(), + closePath: jest.fn(), + stroke: jest.fn(), + translate: jest.fn(), + scale: jest.fn(), + rotate: jest.fn(), + arc: jest.fn(), + fill: jest.fn(), + measureText: jest.fn(() => ({ width: 0 })), + transform: jest.fn(), + rect: jest.fn(), + globalAlpha: 1, + globalCompositeOperation: 'source-over', + filter: 'none', + imageSmoothingEnabled: true, + imageSmoothingQuality: 'low', + strokeStyle: '#000', + fillStyle: '#000', + shadowOffsetX: 0, + shadowOffsetY: 0, + shadowBlur: 0, + shadowColor: 'rgba(0,0,0,0)', + lineWidth: 1, + lineCap: 'butt', + lineJoin: 'miter', + miterLimit: 10, + lineDashOffset: 0, + font: '10px sans-serif', + textAlign: 'start', + textBaseline: 'alphabetic', + direction: 'ltr', + getContextAttributes: jest.fn(() => ({ + alpha: true, + desynchronized: false, + colorSpace: 'srgb', + willReadFrequently: false, + })), +};