Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ export const defaultConfig: ScoutServerConfig = {
`--permissionsPolicy.report_to=${JSON.stringify(['violations-endpoint'])}`,
// Allow dynamic config overrides in tests
`--coreApp.allowDynamicConfigOverrides=true`,
// APM feature flags
'--xpack.apm.featureFlags.serviceMapUseReactFlow=true',
`--xpack.fleet.fleetServerHosts=${JSON.stringify([
{
id: 'default-fleet-server',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ export const defaultConfig: ScoutServerConfig = {
'--xpack.ruleRegistry.write.cache.enabled=false',
'--monitoring_collection.opentelemetry.metrics.prometheus.enabled=true',
'--xpack.profiling.enabled=true',
// APM feature flags
'--xpack.apm.featureFlags.serviceMapUseReactFlow=true',
// Fleet configuration
`--xpack.fleet.fleetServerHosts=${JSON.stringify([
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function ServiceContents({ onFocusClick, elementData, environment, kuery
href={detailsUrl}
fill={true}
>
{i18n.translate('xpack.actions.serviceMap.serviceDetailsButtonText', {
{i18n.translate('xpack.apm.serviceMap.serviceDetailsButtonText', {
defaultMessage: 'Service Details',
})}
</EuiButton>
Expand All @@ -127,7 +127,7 @@ export function ServiceContents({ onFocusClick, elementData, environment, kuery
href={focusUrl}
onClick={onFocusClick}
>
{i18n.translate('xpack.actions.serviceMap.focusMapButtonText', {
{i18n.translate('xpack.apm.serviceMap.focusMapButtonText', {
defaultMessage: 'Focus map',
})}
</EuiButton>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { render, screen } from '@testing-library/react';
import { Position, ReactFlowProvider } from '@xyflow/react';
import { EuiThemeProvider } from '@elastic/eui';
import { DependencyNode, type DependencyMapNodeData } from './dependency_node';

// Mock getSpanIcon to avoid loading actual images
jest.mock('@kbn/apm-ui-shared', () => ({
getSpanIcon: (spanType: string, spanSubtype: string) =>
`/mock-span-icons/${spanType}-${spanSubtype}.svg`,
}));

function renderDependencyNode(
data: Partial<DependencyMapNodeData> & { id: string; label: string },
options: { selected?: boolean } = {}
) {
const nodeProps = {
id: data.id,
type: 'dependency' as const,
data: {
id: data.id,
label: data.label,
spanType: data.spanType ?? '',
spanSubtype: data.spanSubtype ?? '',
} as DependencyMapNodeData,
selected: options.selected ?? false,
sourcePosition: Position.Right,
targetPosition: Position.Left,
zIndex: 0,
isConnectable: true,
xPos: 0,
yPos: 0,
dragging: false,
positionAbsoluteX: 0,
positionAbsoluteY: 0,
};

return render(
<EuiThemeProvider>
<ReactFlowProvider>
<DependencyNode selectable={false} deletable={false} draggable={false} {...nodeProps} />
</ReactFlowProvider>
</EuiThemeProvider>
);
}

describe('DependencyNode', () => {
describe('rendering', () => {
it('renders with the dependency label', () => {
renderDependencyNode({
id: 'postgresql',
label: 'postgresql',
spanType: 'db',
spanSubtype: 'postgresql',
});

expect(screen.getByText('postgresql')).toBeInTheDocument();
});

it('renders span icon when spanType and spanSubtype are provided', () => {
renderDependencyNode({
id: 'postgresql',
label: 'postgresql',
spanType: 'db',
spanSubtype: 'postgresql',
});

const icon = screen.getByRole('img');
expect(icon).toHaveAttribute('src', '/mock-span-icons/db-postgresql.svg');
});

it('does not render icon when spanType is not provided', () => {
renderDependencyNode({
id: 'unknown-dep',
label: 'unknown-dep',
});

expect(screen.queryByRole('img')).not.toBeInTheDocument();
});

it('renders a diamond shape container', () => {
renderDependencyNode({
id: 'postgresql',
label: 'postgresql',
spanType: 'db',
spanSubtype: 'postgresql',
});

// The node should render the dependency node with data-test-subj
expect(screen.getByTestId('serviceMapNode-dependency-postgresql')).toBeInTheDocument();
// And should have the label
expect(screen.getByText('postgresql')).toBeInTheDocument();
});
});

describe('selection state', () => {
it('applies default border color when not selected', () => {
const { container } = renderDependencyNode(
{
id: 'postgresql',
label: 'postgresql',
spanType: 'db',
spanSubtype: 'postgresql',
},
{ selected: false }
);

expect(screen.getByText('postgresql')).toBeInTheDocument();
expect(container).toBeInTheDocument();
});

it('applies primary color styling when selected', () => {
renderDependencyNode(
{
id: 'postgresql',
label: 'postgresql',
spanType: 'db',
spanSubtype: 'postgresql',
},
{ selected: true }
);

// The label should be rendered with selected styling
const labelElement = screen.getByText('postgresql');
expect(labelElement).toBeInTheDocument();
});
});

describe('different dependency types', () => {
const dependencies = [
{ spanType: 'db', spanSubtype: 'postgresql', label: 'postgresql' },
{ spanType: 'db', spanSubtype: 'mysql', label: 'mysql' },
{ spanType: 'cache', spanSubtype: 'redis', label: 'redis' },
{ spanType: 'messaging', spanSubtype: 'kafka', label: 'kafka' },
{ spanType: 'external', spanSubtype: 'http', label: 'external-api' },
{ spanType: 'storage', spanSubtype: 's3', label: 's3-bucket' },
];

dependencies.forEach(({ spanType, spanSubtype, label }) => {
it(`renders ${spanType}/${spanSubtype} dependency icon`, () => {
renderDependencyNode({
id: label,
label,
spanType,
spanSubtype,
});

const icon = screen.getByRole('img');
expect(icon).toHaveAttribute('src', `/mock-span-icons/${spanType}-${spanSubtype}.svg`);
});
});
});

describe('label text handling', () => {
it('renders long labels', () => {
const longLabel = 'very-long-database-connection-string-postgresql-primary';
renderDependencyNode({
id: 'long-dep',
label: longLabel,
spanType: 'db',
spanSubtype: 'postgresql',
});

expect(screen.getByText(longLabel)).toBeInTheDocument();
});

it('renders labels with special characters', () => {
const specialLabel = 'redis:6379';
renderDependencyNode({
id: 'redis-cache',
label: specialLabel,
spanType: 'cache',
spanSubtype: 'redis',
});

expect(screen.getByText(specialLabel)).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useEuiTheme, transparentize, EuiFlexGroup, EuiFlexItem } from '@elastic
import { getSpanIcon } from '@kbn/apm-ui-shared';
import { css } from '@emotion/react';

interface DependencyMapNodeData extends Record<string, any> {
export interface DependencyMapNodeData extends Record<string, any> {
id: string;
label: string;
spanType: string;
Expand All @@ -37,7 +37,13 @@ export const DependencyNode = memo(
}, [data.spanType, data.spanSubtype]);

return (
<EuiFlexGroup direction="column" alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexGroup
direction="column"
alignItems="center"
gutterSize="s"
responsive={false}
data-test-subj={`serviceMapNode-dependency-${data.id}`}
>
{/* Container sized for the rotated diamond visual */}
<EuiFlexItem
grow={false}
Expand Down
Loading
Loading