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 7f5a964e8..8647fe383 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 [
@@ -1295,7 +1295,7 @@ exports[`Trace Detail Render Flyout component render trace detail 1`] = `
},
],
"showlegend": false,
- "width": undefined,
+ "width": 412,
"xaxis": Object {
"color": "#91989c",
"range": Array [
@@ -1365,7 +1365,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 [
@@ -1425,7 +1425,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
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
-
-
-
-
-
-
+ >
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ ,
+]
`;
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 fede7e680..2ca6d2333 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 round from 'lodash/round';
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,
+ })),
+};