Skip to content

Commit cd33fdc

Browse files
[RUM Dashboard] Visitors by region map (#77135)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 1cb09d4 commit cd33fdc

File tree

20 files changed

+1032
-14
lines changed

20 files changed

+1032
-14
lines changed

x-pack/plugins/apm/kibana.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"apmOss",
88
"data",
99
"licensing",
10-
"triggers_actions_ui"
10+
"triggers_actions_ui",
11+
"embeddable"
1112
],
1213
"optionalPlugins": [
1314
"cloud",
@@ -22,15 +23,13 @@
2223
],
2324
"server": true,
2425
"ui": true,
25-
"configPath": [
26-
"xpack",
27-
"apm"
28-
],
26+
"configPath": ["xpack", "apm"],
2927
"extraPublicDirs": ["public/style/variables"],
3028
"requiredBundles": [
3129
"kibanaReact",
3230
"kibanaUtils",
3331
"observability",
34-
"home"
32+
"home",
33+
"maps"
3534
]
3635
}

x-pack/plugins/apm/public/application/csmApp.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import styled, { ThemeProvider, DefaultTheme } from 'styled-components';
1111
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
1212
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
1313
import { CoreStart, AppMountParameters } from 'kibana/public';
14-
import { ApmPluginSetupDeps } from '../plugin';
14+
import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin';
1515

1616
import {
1717
KibanaContextProvider,
@@ -72,11 +72,13 @@ export function CsmAppRoot({
7272
deps,
7373
history,
7474
config,
75+
corePlugins: { embeddable },
7576
}: {
7677
core: CoreStart;
7778
deps: ApmPluginSetupDeps;
7879
history: AppMountParameters['history'];
7980
config: ConfigSchema;
81+
corePlugins: ApmPluginStartDeps;
8082
}) {
8183
const i18nCore = core.i18n;
8284
const plugins = deps;
@@ -88,7 +90,7 @@ export function CsmAppRoot({
8890
return (
8991
<RedirectAppLinks application={core.application}>
9092
<ApmPluginContext.Provider value={apmPluginContextValue}>
91-
<KibanaContextProvider services={{ ...core, ...plugins }}>
93+
<KibanaContextProvider services={{ ...core, ...plugins, embeddable }}>
9294
<i18nCore.Context>
9395
<Router history={history}>
9496
<UrlParamsProvider>
@@ -112,12 +114,19 @@ export const renderApp = (
112114
core: CoreStart,
113115
deps: ApmPluginSetupDeps,
114116
{ element, history }: AppMountParameters,
115-
config: ConfigSchema
117+
config: ConfigSchema,
118+
corePlugins: ApmPluginStartDeps
116119
) => {
117120
createCallApmApi(core.http);
118121

119122
ReactDOM.render(
120-
<CsmAppRoot core={core} deps={deps} history={history} config={config} />,
123+
<CsmAppRoot
124+
core={core}
125+
deps={deps}
126+
history={history}
127+
config={config}
128+
corePlugins={corePlugins}
129+
/>,
121130
element
122131
);
123132
return () => {

x-pack/plugins/apm/public/components/app/RumDashboard/CoreVitals/CoreVitalItem.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ export function CoreVitalItem({
118118
setInFocusInd(ind);
119119
}}
120120
/>
121-
<EuiSpacer size="xl" />
122121
</>
123122
);
124123
}

x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { PageLoadDistribution } from './PageLoadDistribution';
1818
import { I18LABELS } from './translations';
1919
import { VisitorBreakdown } from './VisitorBreakdown';
2020
import { CoreVitals } from './CoreVitals';
21+
import { VisitorBreakdownMap } from './VisitorBreakdownMap';
2122

2223
export function RumDashboard() {
2324
return (
@@ -67,6 +68,9 @@ export function RumDashboard() {
6768
<EuiFlexItem grow={3}>
6869
<VisitorBreakdown />
6970
</EuiFlexItem>
71+
<EuiFlexItem grow={3}>
72+
<VisitorBreakdownMap />
73+
</EuiFlexItem>
7074
</EuiFlexGroup>
7175
</EuiPanel>
7276
</EuiFlexItem>
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { useEffect, useState, useRef } from 'react';
8+
import uuid from 'uuid';
9+
import styled from 'styled-components';
10+
11+
import {
12+
MapEmbeddable,
13+
MapEmbeddableInput,
14+
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
15+
} from '../../../../../../maps/public/embeddable';
16+
import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants';
17+
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
18+
import {
19+
ErrorEmbeddable,
20+
ViewMode,
21+
isErrorEmbeddable,
22+
} from '../../../../../../../../src/plugins/embeddable/public';
23+
import { getLayerList } from './LayerList';
24+
import { useUrlParams } from '../../../../hooks/useUrlParams';
25+
import { RenderTooltipContentParams } from '../../../../../../maps/public';
26+
import { MapToolTip } from './MapToolTip';
27+
import { useMapFilters } from './useMapFilters';
28+
import { EmbeddableStart } from '../../../../../../../../src/plugins/embeddable/public';
29+
30+
const EmbeddedPanel = styled.div`
31+
z-index: auto;
32+
flex: 1;
33+
display: flex;
34+
flex-direction: column;
35+
height: 100%;
36+
position: relative;
37+
.embPanel__content {
38+
display: flex;
39+
flex: 1 1 100%;
40+
z-index: 1;
41+
min-height: 0; // Absolute must for Firefox to scroll contents
42+
}
43+
&&& .mapboxgl-canvas {
44+
animation: none !important;
45+
}
46+
`;
47+
48+
interface KibanaDeps {
49+
embeddable: EmbeddableStart;
50+
}
51+
export function EmbeddedMapComponent() {
52+
const { urlParams } = useUrlParams();
53+
54+
const { start, end, serviceName } = urlParams;
55+
56+
const mapFilters = useMapFilters();
57+
58+
const [embeddable, setEmbeddable] = useState<
59+
MapEmbeddable | ErrorEmbeddable | undefined
60+
>();
61+
62+
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<
63+
HTMLDivElement
64+
>(null);
65+
66+
const {
67+
services: { embeddable: embeddablePlugin },
68+
} = useKibana<KibanaDeps>();
69+
70+
if (!embeddablePlugin) {
71+
throw new Error('Embeddable start plugin not found');
72+
}
73+
const factory: any = embeddablePlugin.getEmbeddableFactory(
74+
MAP_SAVED_OBJECT_TYPE
75+
);
76+
77+
const input: MapEmbeddableInput = {
78+
id: uuid.v4(),
79+
filters: mapFilters,
80+
refreshConfig: {
81+
value: 0,
82+
pause: false,
83+
},
84+
viewMode: ViewMode.VIEW,
85+
isLayerTOCOpen: false,
86+
query: {
87+
query: 'transaction.type : "page-load"',
88+
language: 'kuery',
89+
},
90+
...(start && {
91+
timeRange: {
92+
from: new Date(start!).toISOString(),
93+
to: new Date(end!).toISOString(),
94+
},
95+
}),
96+
hideFilterActions: true,
97+
};
98+
99+
function renderTooltipContent({
100+
addFilters,
101+
closeTooltip,
102+
features,
103+
isLocked,
104+
getLayerName,
105+
loadFeatureProperties,
106+
loadFeatureGeometry,
107+
}: RenderTooltipContentParams) {
108+
const props = {
109+
addFilters,
110+
closeTooltip,
111+
isLocked,
112+
getLayerName,
113+
loadFeatureProperties,
114+
loadFeatureGeometry,
115+
};
116+
117+
return <MapToolTip {...props} features={features} />;
118+
}
119+
120+
useEffect(() => {
121+
if (embeddable != null && serviceName) {
122+
embeddable.updateInput({ filters: mapFilters });
123+
}
124+
// eslint-disable-next-line react-hooks/exhaustive-deps
125+
}, [mapFilters]);
126+
127+
// DateRange updated useEffect
128+
useEffect(() => {
129+
if (embeddable != null && start != null && end != null) {
130+
const timeRange = {
131+
from: new Date(start).toISOString(),
132+
to: new Date(end).toISOString(),
133+
};
134+
embeddable.updateInput({ timeRange });
135+
}
136+
// eslint-disable-next-line react-hooks/exhaustive-deps
137+
}, [start, end]);
138+
139+
useEffect(() => {
140+
async function setupEmbeddable() {
141+
if (!factory) {
142+
throw new Error('Map embeddable not found.');
143+
}
144+
const embeddableObject: any = await factory.create({
145+
...input,
146+
title: 'Visitors by region',
147+
});
148+
149+
if (embeddableObject && !isErrorEmbeddable(embeddableObject)) {
150+
embeddableObject.setRenderTooltipContent(renderTooltipContent);
151+
await embeddableObject.setLayerList(getLayerList());
152+
}
153+
154+
setEmbeddable(embeddableObject);
155+
}
156+
157+
setupEmbeddable();
158+
159+
// we want this effect to execute exactly once after the component mounts
160+
// eslint-disable-next-line react-hooks/exhaustive-deps
161+
}, []);
162+
163+
// We can only render after embeddable has already initialized
164+
useEffect(() => {
165+
if (embeddableRoot.current && embeddable) {
166+
embeddable.render(embeddableRoot.current);
167+
}
168+
}, [embeddable, embeddableRoot]);
169+
170+
return (
171+
<EmbeddedPanel>
172+
<div
173+
data-test-subj="xpack.apm.regionMap.embeddedPanel"
174+
className="embPanel__content"
175+
ref={embeddableRoot}
176+
/>
177+
</EmbeddedPanel>
178+
);
179+
}
180+
181+
EmbeddedMapComponent.displayName = 'EmbeddedMap';
182+
183+
export const EmbeddedMap = React.memo(EmbeddedMapComponent);

0 commit comments

Comments
 (0)