-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[APM] Average latency map for mobile service overview (#144127)
* Add average latency map to mobile service overview
- Loading branch information
Showing
9 changed files
with
512 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
...components/app/service_overview/service_overview_charts/latency_map/embedded_map.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* | ||
* 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 { render } from '@testing-library/react'; | ||
import React from 'react'; | ||
import { EmbeddedMap } from './embedded_map'; | ||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; | ||
import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; | ||
import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; | ||
import { MemoryRouter } from 'react-router-dom'; | ||
|
||
describe('Embedded Map', () => { | ||
it('it renders', async () => { | ||
const mockSetLayerList = jest.fn(); | ||
const mockUpdateInput = jest.fn(); | ||
const mockRender = jest.fn(); | ||
|
||
const mockEmbeddable = embeddablePluginMock.createStartContract(); | ||
mockEmbeddable.getEmbeddableFactory = jest.fn().mockImplementation(() => ({ | ||
create: () => ({ | ||
setLayerList: mockSetLayerList, | ||
updateInput: mockUpdateInput, | ||
render: mockRender, | ||
}), | ||
})); | ||
|
||
const { findByTestId } = render( | ||
<MemoryRouter | ||
initialEntries={[ | ||
'/services/{serviceName}/overview?rangeFrom=now-15m&rangeTo=now&', | ||
]} | ||
> | ||
<MockApmPluginContextWrapper> | ||
<KibanaContextProvider services={{ embeddable: mockEmbeddable }}> | ||
<EmbeddedMap filters={[]} /> | ||
</KibanaContextProvider> | ||
</MockApmPluginContextWrapper> | ||
</MemoryRouter> | ||
); | ||
expect( | ||
await findByTestId('serviceOverviewEmbeddedMap') | ||
).toBeInTheDocument(); | ||
|
||
expect(mockSetLayerList).toHaveBeenCalledTimes(1); | ||
expect(mockUpdateInput).toHaveBeenCalledTimes(1); | ||
expect(mockRender).toHaveBeenCalledTimes(1); | ||
}); | ||
}); |
173 changes: 173 additions & 0 deletions
173
...blic/components/app/service_overview/service_overview_charts/latency_map/embedded_map.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
/* | ||
* 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, { useEffect, useState, useRef } from 'react'; | ||
import uuid from 'uuid'; | ||
import { | ||
MapEmbeddable, | ||
MapEmbeddableInput, | ||
MapEmbeddableOutput, | ||
} from '@kbn/maps-plugin/public'; | ||
import { MAP_SAVED_OBJECT_TYPE } from '@kbn/maps-plugin/common'; | ||
import { | ||
ErrorEmbeddable, | ||
ViewMode, | ||
isErrorEmbeddable, | ||
} from '@kbn/embeddable-plugin/public'; | ||
import { useKibana } from '@kbn/kibana-react-plugin/public'; | ||
import { css } from '@emotion/react'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { EuiText } from '@elastic/eui'; | ||
import type { Filter } from '@kbn/es-query'; | ||
import { ApmPluginStartDeps } from '../../../../../plugin'; | ||
import { getLayerList } from './get_layer_list'; | ||
import { useApmParams } from '../../../../../hooks/use_apm_params'; | ||
import { useTimeRange } from '../../../../../hooks/use_time_range'; | ||
|
||
function EmbeddedMapComponent({ filters }: { filters: Filter[] }) { | ||
const { | ||
query: { rangeFrom, rangeTo, kuery }, | ||
} = useApmParams('/services/{serviceName}/overview'); | ||
|
||
const { start, end } = useTimeRange({ rangeFrom, rangeTo }); | ||
const [error, setError] = useState<boolean>(); | ||
|
||
const [embeddable, setEmbeddable] = useState< | ||
MapEmbeddable | ErrorEmbeddable | undefined | ||
>(); | ||
|
||
const embeddableRoot: React.RefObject<HTMLDivElement> = | ||
useRef<HTMLDivElement>(null); | ||
|
||
const { | ||
embeddable: embeddablePlugin, | ||
maps, | ||
notifications, | ||
} = useKibana<ApmPluginStartDeps>().services; | ||
|
||
useEffect(() => { | ||
async function setupEmbeddable() { | ||
const factory = embeddablePlugin?.getEmbeddableFactory< | ||
MapEmbeddableInput, | ||
MapEmbeddableOutput, | ||
MapEmbeddable | ||
>(MAP_SAVED_OBJECT_TYPE); | ||
|
||
if (!factory) { | ||
setError(true); | ||
notifications?.toasts.addDanger({ | ||
title: i18n.translate( | ||
'xpack.apm.serviceOverview.embeddedMap.error.toastTitle', | ||
{ | ||
defaultMessage: 'An error occurred when adding map embeddable', | ||
} | ||
), | ||
text: i18n.translate( | ||
'xpack.apm.serviceOverview.embeddedMap.error.toastDescription', | ||
{ | ||
defaultMessage: `Embeddable factory with id "{embeddableFactoryId}" was not found.`, | ||
values: { | ||
embeddableFactoryId: MAP_SAVED_OBJECT_TYPE, | ||
}, | ||
} | ||
), | ||
}); | ||
return; | ||
} | ||
|
||
const input: MapEmbeddableInput = { | ||
attributes: { title: '' }, | ||
id: uuid.v4(), | ||
title: i18n.translate( | ||
'xpack.apm.serviceOverview.embeddedMap.input.title', | ||
{ | ||
defaultMessage: 'Latency by country', | ||
} | ||
), | ||
filters, | ||
viewMode: ViewMode.VIEW, | ||
isLayerTOCOpen: false, | ||
query: { | ||
query: kuery, | ||
language: 'kuery', | ||
}, | ||
timeRange: { | ||
from: start, | ||
to: end, | ||
}, | ||
hideFilterActions: true, | ||
}; | ||
|
||
const embeddableObject = await factory.create(input); | ||
if (embeddableObject && !isErrorEmbeddable(embeddableObject)) { | ||
const layerList = await getLayerList(maps); | ||
await embeddableObject.setLayerList(layerList); | ||
} | ||
|
||
setEmbeddable(embeddableObject); | ||
} | ||
|
||
setupEmbeddable(); | ||
// Set up exactly once after the component mounts | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []); | ||
|
||
// We can only render after embeddable has already initialized | ||
useEffect(() => { | ||
if (embeddableRoot.current && embeddable) { | ||
embeddable.render(embeddableRoot.current); | ||
} | ||
}, [embeddable, embeddableRoot]); | ||
|
||
useEffect(() => { | ||
if (embeddable) { | ||
embeddable.updateInput({ | ||
filters, | ||
query: { | ||
query: kuery, | ||
language: 'kuery', | ||
}, | ||
timeRange: { | ||
from: start, | ||
to: end, | ||
}, | ||
}); | ||
} | ||
}, [start, end, kuery, filters, embeddable]); | ||
|
||
return ( | ||
<> | ||
{error && ( | ||
<EuiText size="s"> | ||
<p> | ||
{i18n.translate('xpack.apm.serviceOverview.embeddedMap.error', { | ||
defaultMessage: 'Could not load map', | ||
})} | ||
</p> | ||
</EuiText> | ||
)} | ||
{!error && ( | ||
<div | ||
data-test-subj="serviceOverviewEmbeddedMap" | ||
css={css` | ||
width: 100%; | ||
height: 400px; | ||
display: flex; | ||
flex: 1 1 100%; | ||
z-index: 1; | ||
min-height: 0; | ||
`} | ||
ref={embeddableRoot} | ||
/> | ||
)} | ||
</> | ||
); | ||
} | ||
|
||
EmbeddedMapComponent.displayName = 'EmbeddedMap'; | ||
|
||
export const EmbeddedMap = React.memo(EmbeddedMapComponent); |
151 changes: 151 additions & 0 deletions
151
...lic/components/app/service_overview/service_overview_charts/latency_map/get_layer_list.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
/* | ||
* 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 { | ||
EMSFileSourceDescriptor, | ||
LayerDescriptor as BaseLayerDescriptor, | ||
VectorLayerDescriptor as BaseVectorLayerDescriptor, | ||
VectorStyleDescriptor, | ||
AGG_TYPE, | ||
COLOR_MAP_TYPE, | ||
FIELD_ORIGIN, | ||
LABEL_BORDER_SIZES, | ||
LAYER_TYPE, | ||
SOURCE_TYPES, | ||
STYLE_TYPE, | ||
SYMBOLIZE_AS_TYPES, | ||
} from '@kbn/maps-plugin/common'; | ||
import uuid from 'uuid'; | ||
import type { MapsStartApi } from '@kbn/maps-plugin/public'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { | ||
CLIENT_GEO_COUNTRY_ISO_CODE, | ||
TRANSACTION_DURATION, | ||
} from '../../../../../../common/elasticsearch_fieldnames'; | ||
import { APM_STATIC_DATA_VIEW_ID } from '../../../../../../common/data_view_constants'; | ||
|
||
interface VectorLayerDescriptor extends BaseVectorLayerDescriptor { | ||
sourceDescriptor: EMSFileSourceDescriptor; | ||
} | ||
|
||
const FIELD_NAME = 'apm-service-overview-layer-country'; | ||
const COUNTRY_NAME = 'name'; | ||
const TRANSACTION_DURATION_COUNTRY = `__kbnjoin__avg_of_transaction.duration.us__${FIELD_NAME}`; | ||
|
||
function getLayerStyle(): VectorStyleDescriptor { | ||
return { | ||
type: 'VECTOR', | ||
properties: { | ||
icon: { type: STYLE_TYPE.STATIC, options: { value: 'marker' } }, | ||
fillColor: { | ||
type: STYLE_TYPE.DYNAMIC, | ||
options: { | ||
color: 'Blue to Red', | ||
colorCategory: 'palette_0', | ||
fieldMetaOptions: { isEnabled: true, sigma: 3 }, | ||
type: COLOR_MAP_TYPE.ORDINAL, | ||
field: { | ||
name: TRANSACTION_DURATION_COUNTRY, | ||
origin: FIELD_ORIGIN.JOIN, | ||
}, | ||
useCustomColorRamp: false, | ||
}, | ||
}, | ||
lineColor: { | ||
type: STYLE_TYPE.DYNAMIC, | ||
options: { color: '#3d3d3d', fieldMetaOptions: { isEnabled: true } }, | ||
}, | ||
lineWidth: { type: STYLE_TYPE.STATIC, options: { size: 1 } }, | ||
iconSize: { type: STYLE_TYPE.STATIC, options: { size: 6 } }, | ||
iconOrientation: { | ||
type: STYLE_TYPE.STATIC, | ||
options: { orientation: 0 }, | ||
}, | ||
labelText: { | ||
type: STYLE_TYPE.DYNAMIC, | ||
options: { | ||
field: { | ||
name: TRANSACTION_DURATION_COUNTRY, | ||
origin: FIELD_ORIGIN.JOIN, | ||
}, | ||
}, | ||
}, | ||
labelZoomRange: { | ||
options: { | ||
useLayerZoomRange: true, | ||
minZoom: 0, | ||
maxZoom: 24, | ||
}, | ||
}, | ||
labelColor: { | ||
type: STYLE_TYPE.STATIC, | ||
options: { color: '#000000' }, | ||
}, | ||
labelSize: { type: STYLE_TYPE.STATIC, options: { size: 14 } }, | ||
labelBorderColor: { | ||
type: STYLE_TYPE.STATIC, | ||
options: { color: '#FFFFFF' }, | ||
}, | ||
symbolizeAs: { options: { value: SYMBOLIZE_AS_TYPES.CIRCLE } }, | ||
labelBorderSize: { options: { size: LABEL_BORDER_SIZES.SMALL } }, | ||
}, | ||
isTimeAware: true, | ||
}; | ||
} | ||
|
||
export async function getLayerList(maps?: MapsStartApi) { | ||
const basemapLayerDescriptor = maps | ||
? await maps.createLayerDescriptors.createBasemapLayerDescriptor() | ||
: null; | ||
|
||
const pageLoadDurationByCountryLayer: VectorLayerDescriptor = { | ||
joins: [ | ||
{ | ||
leftField: 'iso2', | ||
right: { | ||
type: SOURCE_TYPES.ES_TERM_SOURCE, | ||
id: FIELD_NAME, | ||
term: CLIENT_GEO_COUNTRY_ISO_CODE, | ||
metrics: [ | ||
{ | ||
type: AGG_TYPE.AVG, | ||
field: TRANSACTION_DURATION, | ||
label: i18n.translate( | ||
'xpack.apm.serviceOverview.embeddedMap.metric.label', | ||
{ | ||
defaultMessage: 'Page load duration', | ||
} | ||
), | ||
}, | ||
], | ||
indexPatternId: APM_STATIC_DATA_VIEW_ID, | ||
applyGlobalQuery: true, | ||
applyGlobalTime: true, | ||
applyForceRefresh: true, | ||
}, | ||
}, | ||
], | ||
sourceDescriptor: { | ||
type: SOURCE_TYPES.EMS_FILE, | ||
id: 'world_countries', | ||
tooltipProperties: [COUNTRY_NAME], | ||
}, | ||
style: getLayerStyle(), | ||
id: uuid.v4(), | ||
label: null, | ||
minZoom: 0, | ||
maxZoom: 24, | ||
alpha: 0.75, | ||
visible: true, | ||
type: LAYER_TYPE.GEOJSON_VECTOR, | ||
}; | ||
|
||
return [ | ||
...(basemapLayerDescriptor ? [basemapLayerDescriptor] : []), | ||
pageLoadDurationByCountryLayer, | ||
] as BaseLayerDescriptor[]; | ||
} |
Oops, something went wrong.