diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx index 731555694bff7ee..52941391ca36415 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx @@ -4,51 +4,52 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiCard, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { storiesOf } from '@storybook/react'; import cytoscape from 'cytoscape'; import React from 'react'; import { Cytoscape } from './Cytoscape'; - -const elements: cytoscape.ElementDefinition[] = [ - { - data: { - id: 'opbeans-python', - label: 'opbeans-python', - agentName: 'python', - type: 'service' - } - }, - { - data: { - id: 'opbeans-node', - label: 'opbeans-node', - agentName: 'nodejs', - type: 'service' - } - }, - { - data: { - id: 'opbeans-ruby', - label: 'opbeans-ruby', - agentName: 'ruby', - type: 'service' - } - }, - { data: { source: 'opbeans-python', target: 'opbeans-node' } }, - { - data: { - bidirectional: true, - source: 'opbeans-python', - target: 'opbeans-ruby' - } - } -]; -const height = 300; -const serviceName = 'opbeans-python'; +import { iconForNode } from './icons'; storiesOf('app/ServiceMap/Cytoscape', module).add( 'example', () => { + const elements: cytoscape.ElementDefinition[] = [ + { + data: { + id: 'opbeans-python', + label: 'opbeans-python', + agentName: 'python', + type: 'service' + } + }, + { + data: { + id: 'opbeans-node', + label: 'opbeans-node', + agentName: 'nodejs', + type: 'service' + } + }, + { + data: { + id: 'opbeans-ruby', + label: 'opbeans-ruby', + agentName: 'ruby', + type: 'service' + } + }, + { data: { source: 'opbeans-python', target: 'opbeans-node' } }, + { + data: { + bidirectional: true, + source: 'opbeans-python', + target: 'opbeans-ruby' + } + } + ]; + const height = 300; + const serviceName = 'opbeans-python'; return ( { + const cy = cytoscape(); + const elements = [ + { data: { id: 'default', label: 'default', type: undefined } }, + { data: { id: 'cache', label: 'cache', type: 'cache' } }, + { data: { id: 'database', label: 'database', type: 'database' } }, + { data: { id: 'external', label: 'external', type: 'external' } }, + { data: { id: 'messaging', label: 'messaging', type: 'messaging' } }, + + { + data: { + id: 'dotnet', + label: 'dotnet service', + type: 'service', + agentName: 'dotnet' + } + }, + { + data: { + id: 'go', + label: 'go service', + type: 'service', + agentName: 'go' + } + }, + { + data: { + id: 'java', + label: 'java service', + type: 'service', + agentName: 'java' + } + }, + { + data: { + id: 'js-base', + label: 'js-base service', + type: 'service', + agentName: 'js-base' + } + }, + { + data: { + id: 'nodejs', + label: 'nodejs service', + type: 'service', + agentName: 'nodejs' + } + }, + { + data: { + id: 'php', + label: 'php service', + type: 'service', + agentName: 'php' + } + }, + { + data: { + id: 'python', + label: 'python service', + type: 'service', + agentName: 'python' + } + }, + { + data: { + id: 'ruby', + label: 'ruby service', + type: 'service', + agentName: 'ruby' + } + } + ]; + cy.add(elements); + + return ( + + {cy.nodes().map(node => ( + + + agentName: {node.data('agentName') || 'undefined'}, type:{' '} + {node.data('type') || 'undefined'} + + } + icon={ + {node.data('label')} + } + title={node.data('label')} + /> + + ))} + + ); + }, + { + info: { + propTables: false, source: false } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index f1c53673c87552a..405bd855898b7aa 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -35,6 +35,7 @@ export function Contents({ onFocusClick, selectedNodeServiceName }: ContentsProps) { + const frameworkName = selectedNodeData.frameworkName; return ( {isService ? ( - + ) : ( )} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index e5962afd76eb8db..23e9e737be9a659 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -16,6 +16,7 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) avgRequestsPerMinute={164.47222031860858} avgCpuUsage={0.32809666568309237} avgMemoryUsage={0.5504868173242986} + frameworkName="Spring" numInstances={2} isLoading={false} /> diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx index b0a5e892b5a7e71..697aa6a1b652b8d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx @@ -11,10 +11,12 @@ import { useUrlParams } from '../../../../hooks/useUrlParams'; import { ServiceMetricList } from './ServiceMetricList'; interface ServiceMetricFetcherProps { + frameworkName?: string; serviceName: string; } export function ServiceMetricFetcher({ + frameworkName, serviceName }: ServiceMetricFetcherProps) { const { @@ -37,5 +39,11 @@ export function ServiceMetricFetcher({ ); const isLoading = status === 'loading'; - return ; + return ( + + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx index 3a6b4c5ebcaac26..b8dd89f45d36ba7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -30,6 +30,10 @@ function LoadingSpinner() { ); } +const BadgeRow = styled(EuiFlexItem)` + padding-bottom: ${lightTheme.gutterTypes.gutterSmall}; +`; + const ItemRow = styled('tr')` line-height: 2; `; @@ -44,6 +48,7 @@ const ItemDescription = styled('td')` `; interface ServiceMetricListProps extends ServiceNodeMetrics { + frameworkName?: string; isLoading: boolean; } @@ -53,6 +58,7 @@ export function ServiceMetricList({ avgErrorsPerMinute, avgCpuUsage, avgMemoryUsage, + frameworkName, numInstances, isLoading }: ServiceMetricListProps) { @@ -106,23 +112,27 @@ export function ServiceMetricList({ : null } ]; + const showBadgeRow = frameworkName || (numInstances && numInstances > 1); + return isLoading ? ( ) : ( <> - {numInstances && numInstances > 1 && ( - -
- - {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', { - values: { numInstances }, - defaultMessage: '{numInstances} instances' - })} - -
-
+ {showBadgeRow && ( + + + {frameworkName && {frameworkName}} + {numInstances && numInstances > 1 && ( + + {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', { + values: { numInstances }, + defaultMessage: '{numInstances} instances' + })} + + )} + + )} - {listItems.map( diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index af5bd17f71ca440..8411169dbc9444c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -42,19 +42,23 @@ const style: cytoscape.Stylesheet[] = [ 'background-image': (el: cytoscape.NodeSingular) => iconForNode(el) ?? defaultIcon, 'background-height': (el: cytoscape.NodeSingular) => - isService(el) ? '85%' : '40%', + isService(el) ? '60%' : '40%', 'background-width': (el: cytoscape.NodeSingular) => - isService(el) ? '85%' : '40%', + isService(el) ? '60%' : '40%', 'border-color': (el: cytoscape.NodeSingular) => el.hasClass('primary') || el.selected() ? theme.euiColorPrimary : theme.euiColorMediumShade, - 'border-width': 1, + 'border-width': 2, color: theme.textColors.default, // theme.euiFontFamily doesn't work here for some reason, so we're just // specifying a subset of the fonts for the label text. 'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif', 'font-size': theme.euiFontSizeXS, + ghost: 'yes', + 'ghost-offset-x': 0, + 'ghost-offset-y': 2, + 'ghost-opacity': 0.15, height: nodeHeight, label: 'data(label)', 'min-zoomed-font-size': theme.euiSizeL, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts index 2403ed047cbc05d..bc619b1ecdfe5f9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts @@ -105,7 +105,8 @@ export function getCytoscapeElements( `/services/${node['service.name']}/service-map`, search ), - agentName: node['agent.name'] || node['agent.name'], + agentName: node['agent.name'], + frameworkName: node['service.framework.name'], type: 'service' }; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts index c637d145639ce9a..1b57cd52082d801 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -4,26 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; import cytoscape from 'cytoscape'; import databaseIcon from './icons/database.svg'; import documentsIcon from './icons/documents.svg'; +import dotNetIcon from './icons/dot-net.svg'; import globeIcon from './icons/globe.svg'; +import goIcon from './icons/go.svg'; +import javaIcon from './icons/java.svg'; +import nodeJsIcon from './icons/nodejs.svg'; +import phpIcon from './icons/php.svg'; +import pythonIcon from './icons/python.svg'; +import rubyIcon from './icons/ruby.svg'; +import rumJsIcon from './icons/rumjs.svg'; +import defaultIconImport from './icons/default.svg'; -function getAvatarIcon( - text = '', - backgroundColor = 'transparent', - foregroundColor = 'white' -) { - return ( - 'data:image/svg+xml;utf8,' + - encodeURIComponent(` - - ${text} - -`) - ); -} +export const defaultIcon = defaultIconImport; // The colors here are taken from the logos of the corresponding technologies const icons: { [key: string]: string } = { @@ -34,18 +29,17 @@ const icons: { [key: string]: string } = { resource: globeIcon }; -const serviceAbbreviations: { [key: string]: string } = { - dotnet: '.N', - go: 'Go', - java: 'Jv', - 'js-base': 'JS', - nodejs: 'No', - python: 'Py', - ruby: 'Rb' +const serviceIcons: { [key: string]: string } = { + dotnet: dotNetIcon, + go: goIcon, + java: javaIcon, + 'js-base': rumJsIcon, + nodejs: nodeJsIcon, + php: phpIcon, + python: pythonIcon, + ruby: rubyIcon }; -export const defaultIcon = getAvatarIcon(); - // IE 11 does not properly load some SVGs, which causes a runtime error and the // map to not work at all. We would prefer to do some kind of feature detection // rather than browser detection, but IE 11 does support SVG, just not well @@ -61,15 +55,12 @@ export function iconForNode(node: cytoscape.NodeSingular) { const type = node.data('type'); if (type === 'service') { - return getAvatarIcon( - serviceAbbreviations[node.data('agentName') as string], - node.selected() || node.hasClass('primary') - ? theme.euiColorPrimary - : theme.euiColorDarkestShade - ); + return serviceIcons[node.data('agentName') as string]; } else if (isIE11) { return defaultIcon; - } else { + } else if (icons[type]) { return icons[type]; + } else { + return defaultIcon; } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg new file mode 100644 index 000000000000000..08bc5331e083b02 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/default.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg new file mode 100644 index 000000000000000..9f7427f0e10017c --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg new file mode 100644 index 000000000000000..fb171e2813fac51 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/go.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg new file mode 100644 index 000000000000000..52a410e2eaa1a64 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/java.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg new file mode 100644 index 000000000000000..d327b1ba65ad26d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg new file mode 100644 index 000000000000000..c8af5dc33126939 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/php.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg new file mode 100644 index 000000000000000..9b8d0a2836c2894 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/python.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg new file mode 100644 index 000000000000000..fdc54b91f9c29ed --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg new file mode 100644 index 000000000000000..87043159ed8c32b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 8f87b3473b2e48b..b4b4e7866e9b7d6 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -66,6 +66,8 @@ exports[`Error SERVICE_AGENT_NAME 1`] = `"java"`; exports[`Error SERVICE_ENVIRONMENT 1`] = `undefined`; +exports[`Error SERVICE_FRAMEWORK_NAME 1`] = `undefined`; + exports[`Error SERVICE_NAME 1`] = `"service name"`; exports[`Error SERVICE_NODE_NAME 1`] = `undefined`; @@ -176,6 +178,8 @@ exports[`Span SERVICE_AGENT_NAME 1`] = `"java"`; exports[`Span SERVICE_ENVIRONMENT 1`] = `undefined`; +exports[`Span SERVICE_FRAMEWORK_NAME 1`] = `undefined`; + exports[`Span SERVICE_NAME 1`] = `"service name"`; exports[`Span SERVICE_NODE_NAME 1`] = `undefined`; @@ -286,6 +290,8 @@ exports[`Transaction SERVICE_AGENT_NAME 1`] = `"java"`; exports[`Transaction SERVICE_ENVIRONMENT 1`] = `undefined`; +exports[`Transaction SERVICE_FRAMEWORK_NAME 1`] = `undefined`; + exports[`Transaction SERVICE_NAME 1`] = `"service name"`; exports[`Transaction SERVICE_NODE_NAME 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index ce2db4964a4120b..14233aad0f53c29 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -7,6 +7,7 @@ export const SERVICE_NAME = 'service.name'; export const SERVICE_ENVIRONMENT = 'service.environment'; export const SERVICE_AGENT_NAME = 'agent.name'; +export const SERVICE_FRAMEWORK_NAME = 'service.framework.name'; export const SERVICE_NODE_NAME = 'service.node.name'; export const SERVICE_VERSION = 'service.version'; export const URL_FULL = 'url.full'; diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 548b29346e4839f..f4354baa97655d4 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -10,6 +10,7 @@ import { ILicense } from '../../licensing/public'; export interface ServiceConnectionNode { 'service.name': string; 'service.environment': string | null; + 'service.framework.name': string | null; 'agent.name': string; } export interface ExternalConnectionNode { diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 04e2a43a4b8f1d5..85d71784b55c708 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -16,7 +16,8 @@ import { getServicesProjection } from '../../../common/projections/services'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { SERVICE_AGENT_NAME, - SERVICE_NAME + SERVICE_NAME, + SERVICE_FRAMEWORK_NAME } from '../../../common/elasticsearch_fieldnames'; export interface IEnvOptions { @@ -92,6 +93,11 @@ async function getServicesData(options: IEnvOptions) { terms: { field: SERVICE_AGENT_NAME } + }, + service_framework_name: { + terms: { + field: SERVICE_FRAMEWORK_NAME + } } } } @@ -109,7 +115,11 @@ async function getServicesData(options: IEnvOptions) { 'service.name': bucket.key as string, 'agent.name': (bucket.agent_name.buckets[0]?.key as string | undefined) || '', - 'service.environment': options.environment || null + 'service.environment': options.environment || null, + 'service.framework.name': + (bucket.service_framework_name.buckets[0]?.key as + | string + | undefined) || null }; }) || [] ); diff --git a/x-pack/plugins/apm/server/lib/services/map.ts b/x-pack/plugins/apm/server/lib/services/map.ts deleted file mode 100644 index 97bb925674e26af..000000000000000 --- a/x-pack/plugins/apm/server/lib/services/map.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import cytoscape from 'cytoscape'; -import { PromiseReturnType } from '../../../typings/common'; - -// This response right now just returns experimental data. -export type ServiceMapResponse = PromiseReturnType; -export async function getServiceMap(): Promise { - return [ - { data: { id: 'client', agentName: 'js-base' } }, - { data: { id: 'opbeans-node', agentName: 'nodejs' } }, - { data: { id: 'opbeans-python', agentName: 'python' } }, - { data: { id: 'opbeans-java', agentName: 'java' } }, - { data: { id: 'opbeans-ruby', agentName: 'ruby' } }, - { data: { id: 'opbeans-go', agentName: 'go' } }, - { data: { id: 'opbeans-go-2', agentName: 'go' } }, - { data: { id: 'opbeans-dotnet', agentName: 'dotnet' } }, - { data: { id: 'database', agentName: 'database' } }, - { data: { id: 'external API', agentName: 'external' } }, - - { - data: { - id: 'opbeans-client~opbeans-node', - source: 'client', - target: 'opbeans-node' - } - }, - { - data: { - id: 'opbeans-client~opbeans-python', - source: 'client', - target: 'opbeans-python' - } - }, - { - data: { - id: 'opbeans-python~opbeans-go', - source: 'opbeans-python', - target: 'opbeans-go' - } - }, - { - data: { - id: 'opbeans-python~opbeans-go-2', - source: 'opbeans-python', - target: 'opbeans-go-2' - } - }, - { - data: { - id: 'opbeans-python~opbeans-dotnet', - source: 'opbeans-python', - target: 'opbeans-dotnet' - } - }, - { - data: { - id: 'opbeans-node~opbeans-java', - source: 'opbeans-node', - target: 'opbeans-java' - } - }, - { - data: { - id: 'opbeans-node~database', - source: 'opbeans-node', - target: 'database' - } - }, - { - data: { - id: 'opbeans-go-2~opbeans-ruby', - source: 'opbeans-go-2', - target: 'opbeans-ruby' - } - }, - { - data: { - id: 'opbeans-go-2~external API', - source: 'opbeans-go-2', - target: 'external API' - } - } - ]; -}