[Maps] observability real user monitoring solution layer (#64949)

* [Maps] observability real user monitoring solution layer

* make stateful component

* rename RUM to observability

* layer select

* metrics select

* display select

* clean up

* create cholopleth map layer descriptor

* get styling working

* fix unique count agg

* create geo grid source

* render as heatmap

* clusters

* tslint errors

* last tslint fix

* only show card when APM index exists

* layer label

* clean up

* fix getLayerWizards

* add parans

* review feedback

* add cluster labels

* use green to red ramp

* tslint fix

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2020-05-05 12:38:52 -06:00 committed by GitHub
parent 415d33b756
commit 891e27eb1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1077 additions and 83 deletions

View file

@ -57,6 +57,7 @@ export enum SOURCE_TYPES {
ES_GEO_GRID = 'ES_GEO_GRID',
ES_SEARCH = 'ES_SEARCH',
ES_PEW_PEW = 'ES_PEW_PEW',
ES_TERM_SOURCE = 'ES_TERM_SOURCE',
EMS_XYZ = 'EMS_XYZ', // identifies a custom TMS source. Name is a little unfortunate.
WMS = 'WMS',
KIBANA_TILEMAP = 'KIBANA_TILEMAP',

View file

@ -71,6 +71,7 @@ export type ESPewPewSourceDescriptor = AbstractESAggSourceDescriptor & {
export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & {
indexPatternTitle: string;
term: string; // term field name
whereQuery?: Query;
};
export type KibanaRegionmapSourceDescriptor = AbstractSourceDescriptor & {

View file

@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { AGG_DELIMITER, AGG_TYPE, JOIN_FIELD_NAME_PREFIX } from './constants';
import { AGG_DELIMITER, AGG_TYPE, COUNT_PROP_NAME, JOIN_FIELD_NAME_PREFIX } from './constants';
// function in common since its needed by migration
export function getJoinAggKey({
aggType,
aggFieldName,
@ -15,8 +14,18 @@ export function getJoinAggKey({
aggType: AGG_TYPE;
aggFieldName?: string;
rightSourceId: string;
}) {
}): string {
const metricKey =
aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${aggFieldName}` : aggType;
return `${JOIN_FIELD_NAME_PREFIX}${metricKey}__${rightSourceId}`;
}
export function getSourceAggKey({
aggType,
aggFieldName,
}: {
aggType: AGG_TYPE;
aggFieldName?: string;
}): string {
return aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${aggFieldName}` : COUNT_PROP_NAME;
}

View file

@ -13,7 +13,7 @@ import {
LAYER_TYPE,
VECTOR_STYLES,
} from '../constants';
import { getJoinAggKey } from '../get_join_key';
import { getJoinAggKey } from '../get_agg_key';
import {
AggDescriptor,
JoinDescriptor,

View file

@ -1,5 +1,5 @@
@import 'gis_map/gis_map';
@import 'layer_addpanel/source_select/index';
@import 'layer_addpanel/index';
@import 'layer_panel/index';
@import 'widget_overlay/index';
@import 'toolbar_overlay/index';

View file

@ -0,0 +1,67 @@
/*
* 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 _ from 'lodash';
import React, { Component, Fragment } from 'react';
import { EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui';
import { getLayerWizards, LayerWizard } from '../../layers/layer_wizard_registry';
interface Props {
onSelect: (layerWizard: LayerWizard) => void;
}
interface State {
layerWizards: LayerWizard[];
}
export class LayerWizardSelect extends Component<Props, State> {
private _isMounted: boolean = false;
state = {
layerWizards: [],
};
componentDidMount() {
this._isMounted = true;
this._loadLayerWizards();
}
componentWillUnmount() {
this._isMounted = false;
}
async _loadLayerWizards() {
const layerWizards = await getLayerWizards();
if (this._isMounted) {
this.setState({ layerWizards });
}
}
render() {
return this.state.layerWizards.map((layerWizard: LayerWizard) => {
const icon = layerWizard.icon ? <EuiIcon type={layerWizard.icon} size="l" /> : undefined;
const onClick = () => {
this.props.onSelect(layerWizard);
};
return (
<Fragment key={layerWizard.title}>
<EuiSpacer size="s" />
<EuiCard
className="mapLayerAddpanel__card"
title={layerWizard.title}
icon={icon}
onClick={onClick}
description={layerWizard.description}
layout="horizontal"
data-test-subj={_.camelCase(layerWizard.title)}
/>
</Fragment>
);
});
}
}

View file

@ -1,41 +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 React, { Fragment } from 'react';
import { getLayerWizards } from '../../../layers/layer_wizard_registry';
import { EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui';
import _ from 'lodash';
export function SourceSelect({ updateSourceSelection }) {
const sourceCards = getLayerWizards().map(layerWizard => {
const icon = layerWizard.icon ? <EuiIcon type={layerWizard.icon} size="l" /> : null;
const onClick = () => {
updateSourceSelection({
layerWizard: layerWizard,
isIndexingSource: !!layerWizard.isIndexingSource,
});
};
return (
<Fragment key={layerWizard.title}>
<EuiSpacer size="s" />
<EuiCard
className="mapLayerAddpanel__card"
title={layerWizard.title}
icon={icon}
onClick={onClick}
description={layerWizard.description}
layout="horizontal"
data-test-subj={_.camelCase(layerWizard.title)}
/>
</Fragment>
);
});
return <Fragment>{sourceCards}</Fragment>;
}

View file

@ -5,7 +5,7 @@
*/
import React, { Component, Fragment } from 'react';
import { SourceSelect } from './source_select/source_select';
import { LayerWizardSelect } from './layer_wizard_select';
import { FlyoutFooter } from './flyout_footer';
import { ImportEditor } from './import_editor';
import { EuiButtonEmpty, EuiPanel, EuiTitle, EuiFlyoutHeader, EuiSpacer } from '@elastic/eui';
@ -80,8 +80,8 @@ export class AddLayerPanel extends Component {
this.props.removeTransientLayer();
};
_onSourceSelectionChange = ({ layerWizard, isIndexingSource }) => {
this.setState({ layerWizard, importView: isIndexingSource });
_onWizardSelect = layerWizard => {
this.setState({ layerWizard, importView: !!layerWizard.isIndexingSource });
};
_layerAddHandler = () => {
@ -100,7 +100,7 @@ export class AddLayerPanel extends Component {
_renderPanelBody() {
if (!this.state.layerWizard) {
return <SourceSelect updateSourceSelection={this._onSourceSelectionChange} />;
return <LayerWizardSelect onSelect={this._onWizardSelect} />;
}
const backButton = this.props.isIndexingTriggered ? null : (

View file

@ -20,7 +20,7 @@ export type RenderWizardArguments = {
};
export type LayerWizard = {
checkVisibility?: () => boolean;
checkVisibility?: () => Promise<boolean>;
description: string;
icon: string;
isIndexingSource?: boolean;
@ -31,11 +31,23 @@ export type LayerWizard = {
const registry: LayerWizard[] = [];
export function registerLayerWizard(layerWizard: LayerWizard) {
registry.push(layerWizard);
}
export function getLayerWizards(): LayerWizard[] {
return registry.filter(layerWizard => {
return layerWizard.checkVisibility ? layerWizard.checkVisibility() : true;
registry.push({
checkVisibility: async () => {
return true;
},
...layerWizard,
});
}
export async function getLayerWizards(): Promise<LayerWizard[]> {
const promises = registry.map(async layerWizard => {
return {
...layerWizard,
// @ts-ignore
isVisible: await layerWizard.checkVisibility(),
};
});
return (await Promise.all(promises)).filter(({ isVisible }) => {
return isVisible;
});
}

View file

@ -25,17 +25,19 @@ import { tmsLayerWizardConfig } from './sources/xyz_tms_source';
// @ts-ignore
import { wmsLayerWizardConfig } from './sources/wms_source';
import { mvtVectorSourceWizardConfig } from './sources/mvt_single_layer_vector_source';
// @ts-ignore
import { ObservabilityLayerWizardConfig } from './solution_layers/observability';
import { getInjectedVarFunc } from '../kibana_services';
// Registration order determines display order
let registered = false;
export function registerLayerWizards() {
if (registered) {
return;
}
// Registration order determines display order
// @ts-ignore
registerLayerWizard(uploadLayerWizardConfig);
registerLayerWizard(ObservabilityLayerWizardConfig);
// @ts-ignore
registerLayerWizard(esDocumentsLayerWizardConfig);
// @ts-ignore

View file

@ -0,0 +1,336 @@
/*
* 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.
*/
jest.mock('../../../kibana_services', () => {
const mockUiSettings = {
get: () => {
return undefined;
},
};
return {
getUiSettings: () => {
return mockUiSettings;
},
};
});
jest.mock('uuid/v4', () => {
return function() {
return '12345';
};
});
import { createLayerDescriptor } from './create_layer_descriptor';
import { OBSERVABILITY_LAYER_TYPE } from './layer_select';
import { OBSERVABILITY_METRIC_TYPE } from './metric_select';
import { DISPLAY } from './display_select';
describe('createLayerDescriptor', () => {
test('Should create vector layer descriptor with join when displayed as choropleth', () => {
const layerDescriptor = createLayerDescriptor({
layer: OBSERVABILITY_LAYER_TYPE.APM_RUM_PERFORMANCE,
metric: OBSERVABILITY_METRIC_TYPE.TRANSACTION_DURATION,
display: DISPLAY.CHOROPLETH,
});
expect(layerDescriptor).toEqual({
__dataRequests: [],
alpha: 0.75,
id: '12345',
joins: [
{
leftField: 'iso2',
right: {
id: '12345',
indexPatternId: 'apm_static_index_pattern_id',
indexPatternTitle: 'apm-*',
metrics: [
{
field: 'transaction.duration.us',
type: 'avg',
},
],
term: 'client.geo.country_iso_code',
type: 'ES_TERM_SOURCE',
whereQuery: {
language: 'kuery',
query: 'processor.event:"transaction"',
},
},
},
],
label: '[Performance] Duration',
maxZoom: 24,
minZoom: 0,
sourceDescriptor: {
id: 'world_countries',
tooltipProperties: ['name', 'iso2'],
type: 'EMS_FILE',
},
style: {
isTimeAware: true,
properties: {
fillColor: {
options: {
color: 'Green to Red',
colorCategory: 'palette_0',
field: {
name: '__kbnjoin__avg_of_transaction.duration.us__12345',
origin: 'join',
},
fieldMetaOptions: {
isEnabled: true,
sigma: 3,
},
type: 'ORDINAL',
},
type: 'DYNAMIC',
},
icon: {
options: {
value: 'marker',
},
type: 'STATIC',
},
iconOrientation: {
options: {
orientation: 0,
},
type: 'STATIC',
},
iconSize: {
options: {
size: 6,
},
type: 'STATIC',
},
labelText: {
options: {
value: '',
},
type: 'STATIC',
},
labelBorderColor: {
options: {
color: '#FFFFFF',
},
type: 'STATIC',
},
labelBorderSize: {
options: {
size: 'SMALL',
},
},
labelColor: {
options: {
color: '#000000',
},
type: 'STATIC',
},
labelSize: {
options: {
size: 14,
},
type: 'STATIC',
},
lineColor: {
options: {
color: '#3d3d3d',
},
type: 'STATIC',
},
lineWidth: {
options: {
size: 1,
},
type: 'STATIC',
},
symbolizeAs: {
options: {
value: 'circle',
},
},
},
type: 'VECTOR',
},
type: 'VECTOR',
visible: true,
});
});
test('Should create heatmap layer descriptor when displayed as heatmap', () => {
const layerDescriptor = createLayerDescriptor({
layer: OBSERVABILITY_LAYER_TYPE.APM_RUM_PERFORMANCE,
metric: OBSERVABILITY_METRIC_TYPE.TRANSACTION_DURATION,
display: DISPLAY.HEATMAP,
});
expect(layerDescriptor).toEqual({
__dataRequests: [],
alpha: 0.75,
id: '12345',
joins: [],
label: '[Performance] Duration',
maxZoom: 24,
minZoom: 0,
query: {
language: 'kuery',
query: 'processor.event:"transaction"',
},
sourceDescriptor: {
geoField: 'client.geo.location',
id: '12345',
indexPatternId: 'apm_static_index_pattern_id',
metrics: [
{
field: 'transaction.duration.us',
type: 'avg',
},
],
requestType: 'heatmap',
resolution: 'MOST_FINE',
type: 'ES_GEO_GRID',
},
style: {
colorRampName: 'theclassic',
type: 'HEATMAP',
},
type: 'HEATMAP',
visible: true,
});
});
test('Should create vector layer descriptor when displayed as clusters', () => {
const layerDescriptor = createLayerDescriptor({
layer: OBSERVABILITY_LAYER_TYPE.APM_RUM_PERFORMANCE,
metric: OBSERVABILITY_METRIC_TYPE.TRANSACTION_DURATION,
display: DISPLAY.CLUSTERS,
});
expect(layerDescriptor).toEqual({
__dataRequests: [],
alpha: 0.75,
id: '12345',
joins: [],
label: '[Performance] Duration',
maxZoom: 24,
minZoom: 0,
query: {
language: 'kuery',
query: 'processor.event:"transaction"',
},
sourceDescriptor: {
geoField: 'client.geo.location',
id: '12345',
indexPatternId: 'apm_static_index_pattern_id',
metrics: [
{
field: 'transaction.duration.us',
type: 'avg',
},
],
requestType: 'point',
resolution: 'MOST_FINE',
type: 'ES_GEO_GRID',
},
style: {
isTimeAware: true,
properties: {
fillColor: {
options: {
color: 'Green to Red',
colorCategory: 'palette_0',
field: {
name: 'avg_of_transaction.duration.us',
origin: 'source',
},
fieldMetaOptions: {
isEnabled: true,
sigma: 3,
},
type: 'ORDINAL',
},
type: 'DYNAMIC',
},
icon: {
options: {
value: 'marker',
},
type: 'STATIC',
},
iconOrientation: {
options: {
orientation: 0,
},
type: 'STATIC',
},
iconSize: {
options: {
field: {
name: 'avg_of_transaction.duration.us',
origin: 'source',
},
fieldMetaOptions: {
isEnabled: true,
sigma: 3,
},
maxSize: 32,
minSize: 7,
},
type: 'DYNAMIC',
},
labelText: {
options: {
value: '',
},
type: 'STATIC',
},
labelBorderColor: {
options: {
color: '#FFFFFF',
},
type: 'STATIC',
},
labelBorderSize: {
options: {
size: 'SMALL',
},
},
labelColor: {
options: {
color: '#000000',
},
type: 'STATIC',
},
labelSize: {
options: {
size: 14,
},
type: 'STATIC',
},
lineColor: {
options: {
color: '#3d3d3d',
},
type: 'STATIC',
},
lineWidth: {
options: {
size: 1,
},
type: 'STATIC',
},
symbolizeAs: {
options: {
value: 'circle',
},
},
},
type: 'VECTOR',
},
type: 'VECTOR',
visible: true,
});
});
});

View file

@ -0,0 +1,270 @@
/*
* 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 uuid from 'uuid/v4';
import { i18n } from '@kbn/i18n';
import {
AggDescriptor,
ColorDynamicOptions,
LabelDynamicOptions,
LayerDescriptor,
SizeDynamicOptions,
StylePropertyField,
VectorStylePropertiesDescriptor,
} from '../../../../common/descriptor_types';
import {
AGG_TYPE,
COLOR_MAP_TYPE,
FIELD_ORIGIN,
GRID_RESOLUTION,
RENDER_AS,
SOURCE_TYPES,
STYLE_TYPE,
VECTOR_STYLES,
} from '../../../../common/constants';
import { getJoinAggKey, getSourceAggKey } from '../../../../common/get_agg_key';
import { OBSERVABILITY_LAYER_TYPE } from './layer_select';
import { OBSERVABILITY_METRIC_TYPE } from './metric_select';
import { DISPLAY } from './display_select';
import { VectorStyle } from '../../styles/vector/vector_style';
// @ts-ignore
import { EMSFileSource } from '../../sources/ems_file_source';
// @ts-ignore
import { ESGeoGridSource } from '../../sources/es_geo_grid_source';
import { VectorLayer } from '../../vector_layer';
// @ts-ignore
import { HeatmapLayer } from '../../heatmap_layer';
import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults';
// redefining APM constant to avoid making maps app depend on APM plugin
export const APM_INDEX_PATTERN_ID = 'apm_static_index_pattern_id';
const defaultDynamicProperties = getDefaultDynamicProperties();
function createDynamicFillColorDescriptor(
layer: OBSERVABILITY_LAYER_TYPE,
field: StylePropertyField
) {
return {
type: STYLE_TYPE.DYNAMIC,
options: {
...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]!.options as ColorDynamicOptions),
field,
color:
layer === OBSERVABILITY_LAYER_TYPE.APM_RUM_PERFORMANCE ? 'Green to Red' : 'Yellow to Red',
type: COLOR_MAP_TYPE.ORDINAL,
},
};
}
function createLayerLabel(
layer: OBSERVABILITY_LAYER_TYPE,
metric: OBSERVABILITY_METRIC_TYPE
): string | null {
let layerName;
if (layer === OBSERVABILITY_LAYER_TYPE.APM_RUM_PERFORMANCE) {
layerName = i18n.translate('xpack.maps.observability.apmRumPerformanceLayerName', {
defaultMessage: 'Performance',
});
} else if (layer === OBSERVABILITY_LAYER_TYPE.APM_RUM_TRAFFIC) {
layerName = i18n.translate('xpack.maps.observability.apmRumTrafficLayerName', {
defaultMessage: 'Traffic',
});
}
let metricName;
if (metric === OBSERVABILITY_METRIC_TYPE.TRANSACTION_DURATION) {
metricName = i18n.translate('xpack.maps.observability.durationMetricName', {
defaultMessage: 'Duration',
});
} else if (metric === OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE) {
metricName = i18n.translate('xpack.maps.observability.slaPercentageMetricName', {
defaultMessage: '% Duration of SLA',
});
} else if (metric === OBSERVABILITY_METRIC_TYPE.COUNT) {
metricName = i18n.translate('xpack.maps.observability.countMetricName', {
defaultMessage: 'Total',
});
} else if (metric === OBSERVABILITY_METRIC_TYPE.UNIQUE_COUNT) {
metricName = i18n.translate('xpack.maps.observability.uniqueCountMetricName', {
defaultMessage: 'Unique count',
});
}
return `[${layerName}] ${metricName}`;
}
function createAggDescriptor(metric: OBSERVABILITY_METRIC_TYPE): AggDescriptor {
if (metric === OBSERVABILITY_METRIC_TYPE.TRANSACTION_DURATION) {
return {
type: AGG_TYPE.AVG,
field: 'transaction.duration.us',
};
} else if (metric === OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE) {
return {
type: AGG_TYPE.AVG,
field: 'duration_sla_pct',
};
} else if (metric === OBSERVABILITY_METRIC_TYPE.UNIQUE_COUNT) {
return {
type: AGG_TYPE.UNIQUE_COUNT,
field: 'transaction.id',
};
} else {
return { type: AGG_TYPE.COUNT };
}
}
// All APM indices match APM index pattern. Need to apply query to filter to specific document types
// https://www.elastic.co/guide/en/kibana/current/apm-settings-kb.html
function createAmpSourceQuery(layer: OBSERVABILITY_LAYER_TYPE) {
// APM transaction documents
let query;
if (
layer === OBSERVABILITY_LAYER_TYPE.APM_RUM_PERFORMANCE ||
layer === OBSERVABILITY_LAYER_TYPE.APM_RUM_TRAFFIC
) {
query = 'processor.event:"transaction"';
}
return query
? {
language: 'kuery',
query,
}
: undefined;
}
function getGeoGridRequestType(display: DISPLAY): RENDER_AS {
if (display === DISPLAY.HEATMAP) {
return RENDER_AS.HEATMAP;
}
if (display === DISPLAY.GRIDS) {
return RENDER_AS.GRID;
}
return RENDER_AS.POINT;
}
export function createLayerDescriptor({
layer,
metric,
display,
}: {
layer: OBSERVABILITY_LAYER_TYPE | null;
metric: OBSERVABILITY_METRIC_TYPE | null;
display: DISPLAY | null;
}): LayerDescriptor | null {
if (!layer || !metric || !display) {
return null;
}
const apmSourceQuery = createAmpSourceQuery(layer);
const label = createLayerLabel(layer, metric);
const metricsDescriptor = createAggDescriptor(metric);
if (display === DISPLAY.CHOROPLETH) {
const joinId = uuid();
const joinKey = getJoinAggKey({
aggType: metricsDescriptor.type,
aggFieldName: metricsDescriptor.field ? metricsDescriptor.field : '',
rightSourceId: joinId,
});
return VectorLayer.createDescriptor({
label,
joins: [
{
leftField: 'iso2',
right: {
type: SOURCE_TYPES.ES_TERM_SOURCE,
id: joinId,
indexPatternId: APM_INDEX_PATTERN_ID,
indexPatternTitle: 'apm-*', // TODO look up from APM_OSS.indexPattern
term: 'client.geo.country_iso_code',
metrics: [metricsDescriptor],
whereQuery: apmSourceQuery,
},
},
],
sourceDescriptor: EMSFileSource.createDescriptor({
id: 'world_countries',
tooltipProperties: ['name', 'iso2'],
}),
style: VectorStyle.createDescriptor({
[VECTOR_STYLES.FILL_COLOR]: createDynamicFillColorDescriptor(layer, {
name: joinKey,
origin: FIELD_ORIGIN.JOIN,
}),
[VECTOR_STYLES.LINE_COLOR]: {
type: STYLE_TYPE.STATIC,
options: {
color: '#3d3d3d',
},
},
}),
});
}
const geoGridSourceDescriptor = ESGeoGridSource.createDescriptor({
indexPatternId: APM_INDEX_PATTERN_ID,
geoField: 'client.geo.location',
metrics: [metricsDescriptor],
requestType: getGeoGridRequestType(display),
resolution: GRID_RESOLUTION.MOST_FINE,
});
if (display === DISPLAY.HEATMAP) {
return HeatmapLayer.createDescriptor({
label,
query: apmSourceQuery,
sourceDescriptor: geoGridSourceDescriptor,
});
}
const metricSourceKey = getSourceAggKey({
aggType: metricsDescriptor.type,
aggFieldName: metricsDescriptor.field,
});
const metricStyleField = {
name: metricSourceKey,
origin: FIELD_ORIGIN.SOURCE,
};
const styleProperties: VectorStylePropertiesDescriptor = {
[VECTOR_STYLES.FILL_COLOR]: createDynamicFillColorDescriptor(layer, metricStyleField),
[VECTOR_STYLES.ICON_SIZE]: {
type: STYLE_TYPE.DYNAMIC,
options: {
...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions),
field: metricStyleField,
},
},
[VECTOR_STYLES.LINE_COLOR]: {
type: STYLE_TYPE.STATIC,
options: {
color: '#3d3d3d',
},
},
};
if (metric === OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE) {
styleProperties[VECTOR_STYLES.LABEL_TEXT] = {
type: STYLE_TYPE.DYNAMIC,
options: {
...(defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT]!.options as LabelDynamicOptions),
field: metricStyleField,
},
};
}
return VectorLayer.createDescriptor({
label,
query: apmSourceQuery,
sourceDescriptor: geoGridSourceDescriptor,
style: VectorStyle.createDescriptor(styleProperties),
});
}

View file

@ -0,0 +1,70 @@
/*
* 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 React, { ChangeEvent } from 'react';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { OBSERVABILITY_LAYER_TYPE } from './layer_select';
export enum DISPLAY {
CHOROPLETH = 'CHOROPLETH',
CLUSTERS = 'CLUSTERS',
GRIDS = 'GRIDS',
HEATMAP = 'HEATMAP',
}
const DISPLAY_OPTIONS = [
{
value: DISPLAY.CHOROPLETH,
text: i18n.translate('xpack.maps.observability.choroplethLabel', {
defaultMessage: 'World boundaries',
}),
},
{
value: DISPLAY.CLUSTERS,
text: i18n.translate('xpack.maps.observability.clustersLabel', {
defaultMessage: 'Clusters',
}),
},
{
value: DISPLAY.GRIDS,
text: i18n.translate('xpack.maps.observability.gridsLabel', {
defaultMessage: 'Grids',
}),
},
{
value: DISPLAY.HEATMAP,
text: i18n.translate('xpack.maps.observability.heatMapLabel', {
defaultMessage: 'Heat map',
}),
},
];
interface Props {
layer: OBSERVABILITY_LAYER_TYPE | null;
value: DISPLAY;
onChange: (display: DISPLAY) => void;
}
export function DisplaySelect(props: Props) {
function onChange(event: ChangeEvent<HTMLSelectElement>) {
props.onChange(event.target.value as DISPLAY);
}
if (!props.layer) {
return null;
}
return (
<EuiFormRow
label={i18n.translate('xpack.maps.observability.displayLabel', {
defaultMessage: 'Display',
})}
>
<EuiSelect options={DISPLAY_OPTIONS} value={props.value} onChange={onChange} />
</EuiFormRow>
);
}

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { ObservabilityLayerWizardConfig } from './observability_layer_wizard';

View file

@ -0,0 +1,54 @@
/*
* 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 React, { ChangeEvent } from 'react';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export enum OBSERVABILITY_LAYER_TYPE {
APM_RUM_PERFORMANCE = 'APM_RUM_PERFORMANCE',
APM_RUM_TRAFFIC = 'APM_RUM_TRAFFIC',
}
const OBSERVABILITY_LAYER_OPTIONS = [
{
value: OBSERVABILITY_LAYER_TYPE.APM_RUM_PERFORMANCE,
text: i18n.translate('xpack.maps.observability.apmRumPerformanceLabel', {
defaultMessage: 'APM RUM Performance',
}),
},
{
value: OBSERVABILITY_LAYER_TYPE.APM_RUM_TRAFFIC,
text: i18n.translate('xpack.maps.observability.apmRumTrafficLabel', {
defaultMessage: 'APM RUM Traffic',
}),
},
];
interface Props {
value: OBSERVABILITY_LAYER_TYPE | null;
onChange: (layer: OBSERVABILITY_LAYER_TYPE) => void;
}
export function LayerSelect(props: Props) {
function onChange(event: ChangeEvent<HTMLSelectElement>) {
props.onChange(event.target.value as OBSERVABILITY_LAYER_TYPE);
}
const options = props.value
? OBSERVABILITY_LAYER_OPTIONS
: [{ value: '', text: '' }, ...OBSERVABILITY_LAYER_OPTIONS];
return (
<EuiFormRow
label={i18n.translate('xpack.maps.observability.layerLabel', {
defaultMessage: 'Layer',
})}
>
<EuiSelect options={options} value={props.value ? props.value : ''} onChange={onChange} />
</EuiFormRow>
);
}

View file

@ -0,0 +1,89 @@
/*
* 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 React, { ChangeEvent } from 'react';
import { EuiFormRow, EuiSelect, EuiSelectOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { OBSERVABILITY_LAYER_TYPE } from './layer_select';
export enum OBSERVABILITY_METRIC_TYPE {
TRANSACTION_DURATION = 'TRANSACTION_DURATION',
SLA_PERCENTAGE = 'SLA_PERCENTAGE',
COUNT = 'COUNT',
UNIQUE_COUNT = 'UNIQUE_COUNT',
}
const APM_RUM_PERFORMANCE_METRIC_OPTIONS = [
{
value: OBSERVABILITY_METRIC_TYPE.TRANSACTION_DURATION,
text: i18n.translate('xpack.maps.observability.transactionDurationLabel', {
defaultMessage: 'Transaction duraction',
}),
},
{
value: OBSERVABILITY_METRIC_TYPE.SLA_PERCENTAGE,
text: i18n.translate('xpack.maps.observability.slaPercentageLabel', {
defaultMessage: 'SLA percentage',
}),
},
];
const APM_RUM_TRAFFIC_METRIC_OPTIONS = [
{
value: OBSERVABILITY_METRIC_TYPE.COUNT,
text: i18n.translate('xpack.maps.observability.countLabel', {
defaultMessage: 'Count',
}),
},
{
value: OBSERVABILITY_METRIC_TYPE.UNIQUE_COUNT,
text: i18n.translate('xpack.maps.observability.uniqueCountLabel', {
defaultMessage: 'Unique count',
}),
},
];
export function getMetricOptionsForLayer(layer: OBSERVABILITY_LAYER_TYPE): EuiSelectOption[] {
if (layer === OBSERVABILITY_LAYER_TYPE.APM_RUM_PERFORMANCE) {
return APM_RUM_PERFORMANCE_METRIC_OPTIONS;
}
if (layer === OBSERVABILITY_LAYER_TYPE.APM_RUM_TRAFFIC) {
return APM_RUM_TRAFFIC_METRIC_OPTIONS;
}
return [];
}
interface Props {
layer: OBSERVABILITY_LAYER_TYPE | null;
value: OBSERVABILITY_METRIC_TYPE | null;
onChange: (metricType: OBSERVABILITY_METRIC_TYPE) => void;
}
export function MetricSelect(props: Props) {
function onChange(event: ChangeEvent<HTMLSelectElement>) {
props.onChange(event.target.value as OBSERVABILITY_METRIC_TYPE);
}
if (!props.layer || !props.value) {
return null;
}
return (
<EuiFormRow
label={i18n.translate('xpack.maps.observability.metricLabel', {
defaultMessage: 'Metric',
})}
>
<EuiSelect
options={getMetricOptionsForLayer(props.layer)}
value={props.value}
onChange={onChange}
/>
</EuiFormRow>
);
}

View file

@ -0,0 +1,82 @@
/*
* 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 React, { Component, Fragment } from 'react';
import { RenderWizardArguments } from '../../layer_wizard_registry';
import { LayerSelect, OBSERVABILITY_LAYER_TYPE } from './layer_select';
import { getMetricOptionsForLayer, MetricSelect, OBSERVABILITY_METRIC_TYPE } from './metric_select';
import { DisplaySelect, DISPLAY } from './display_select';
import { createLayerDescriptor } from './create_layer_descriptor';
interface State {
display: DISPLAY;
layer: OBSERVABILITY_LAYER_TYPE | null;
metric: OBSERVABILITY_METRIC_TYPE | null;
}
export class ObservabilityLayerTemplate extends Component<RenderWizardArguments, State> {
state = {
layer: null,
metric: null,
display: DISPLAY.CHOROPLETH,
};
_onLayerChange = (layer: OBSERVABILITY_LAYER_TYPE) => {
const newState = { layer, metric: this.state.metric };
// Select metric when layer change invalidates selected metric.
const metricOptions = getMetricOptionsForLayer(layer);
const selectedMetricOption = metricOptions.find(option => {
return option.value === this.state.metric;
});
if (!selectedMetricOption) {
if (metricOptions.length) {
// @ts-ignore
newState.metric = metricOptions[0].value;
} else {
newState.metric = null;
}
}
this.setState(newState, this._previewLayer);
};
_onMetricChange = (metric: OBSERVABILITY_METRIC_TYPE) => {
this.setState({ metric }, this._previewLayer);
};
_onDisplayChange = (display: DISPLAY) => {
this.setState({ display }, this._previewLayer);
};
_previewLayer() {
this.props.previewLayer(
createLayerDescriptor({
layer: this.state.layer,
metric: this.state.metric,
display: this.state.display,
})
);
}
render() {
return (
<Fragment>
<LayerSelect value={this.state.layer} onChange={this._onLayerChange} />
<MetricSelect
layer={this.state.layer}
value={this.state.metric}
onChange={this._onMetricChange}
/>
<DisplaySelect
layer={this.state.layer}
value={this.state.display}
onChange={this._onDisplayChange}
/>
</Fragment>
);
}
}

View file

@ -0,0 +1,33 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry';
import { ObservabilityLayerTemplate } from './observability_layer_template';
import { APM_INDEX_PATTERN_ID } from './create_layer_descriptor';
import { getIndexPatternService } from '../../../kibana_services';
export const ObservabilityLayerWizardConfig: LayerWizard = {
checkVisibility: async () => {
try {
await getIndexPatternService().get(APM_INDEX_PATTERN_ID);
return true;
} catch (e) {
return false;
}
},
description: i18n.translate('xpack.maps.observability.desc', {
defaultMessage: 'APM layers',
}),
icon: 'logoObservability',
renderWizard: (renderWizardArguments: RenderWizardArguments) => {
return <ObservabilityLayerTemplate {...renderWizardArguments} />;
},
title: i18n.translate('xpack.maps.observability.title', {
defaultMessage: 'Observability',
}),
};

View file

@ -7,13 +7,8 @@
import { i18n } from '@kbn/i18n';
import { AbstractESSource } from '../es_source';
import { esAggFieldsFactory } from '../../fields/es_agg_field';
import {
AGG_DELIMITER,
AGG_TYPE,
COUNT_PROP_LABEL,
COUNT_PROP_NAME,
FIELD_ORIGIN,
} from '../../../../common/constants';
import { AGG_TYPE, COUNT_PROP_LABEL, FIELD_ORIGIN } from '../../../../common/constants';
import { getSourceAggKey } from '../../../../common/get_agg_key';
export class AbstractESAggSource extends AbstractESSource {
constructor(descriptor, inspectorAdapters) {
@ -59,7 +54,10 @@ export class AbstractESAggSource extends AbstractESSource {
}
getAggKey(aggType, fieldName) {
return aggType !== AGG_TYPE.COUNT ? `${aggType}${AGG_DELIMITER}${fieldName}` : COUNT_PROP_NAME;
return getSourceAggKey({
aggType,
aggFieldName: fieldName,
});
}
getAggLabel(aggType, fieldName) {

View file

@ -35,13 +35,14 @@ export const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle
export class ESGeoGridSource extends AbstractESAggSource {
static type = SOURCE_TYPES.ES_GEO_GRID;
static createDescriptor({ indexPatternId, geoField, requestType, resolution }) {
static createDescriptor({ indexPatternId, geoField, metrics, requestType, resolution }) {
return {
type: ESGeoGridSource.type,
id: uuid(),
indexPatternId: indexPatternId,
geoField: geoField,
requestType: requestType,
indexPatternId,
geoField,
metrics: metrics ? metrics : [],
requestType,
resolution: resolution ? resolution : GRID_RESOLUTION.COARSE,
};
}

View file

@ -18,9 +18,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
// @ts-ignore
import { SingleFieldSelect } from '../../../components/single_field_select';
// @ts-ignore
import { indexPatternService } from '../../../kibana_services';
import { getIndexPatternService } from '../../../kibana_services';
// @ts-ignore
import { ValidatedRange } from '../../../components/validated_range';
import {
@ -68,8 +66,10 @@ export class ScalingForm extends Component<Props, State> {
async loadIndexSettings() {
try {
const indexPattern = await indexPatternService.get(this.props.indexPatternId);
const { maxInnerResultWindow, maxResultWindow } = await loadIndexSettings(indexPattern.title);
const indexPattern = await getIndexPatternService().get(this.props.indexPatternId);
const { maxInnerResultWindow, maxResultWindow } = await loadIndexSettings(
indexPattern!.title
);
if (this._isMounted) {
this.setState({ maxInnerResultWindow, maxResultWindow });
}

View file

@ -7,8 +7,13 @@
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { AGG_TYPE, DEFAULT_MAX_BUCKETS_LIMIT, FIELD_ORIGIN } from '../../../../common/constants';
import { getJoinAggKey } from '../../../../common/get_join_key';
import {
AGG_TYPE,
DEFAULT_MAX_BUCKETS_LIMIT,
FIELD_ORIGIN,
SOURCE_TYPES,
} from '../../../../common/constants';
import { getJoinAggKey } from '../../../../common/get_agg_key';
import { ESDocField } from '../../fields/es_doc_field';
import { AbstractESAggSource } from '../es_agg_source';
import { getField, addFieldToDSL, extractPropertiesFromBucket } from '../../util/es_agg_utils';
@ -30,7 +35,7 @@ export function extractPropertiesMap(rawEsData, countPropertyName) {
}
export class ESTermSource extends AbstractESAggSource {
static type = 'ES_TERM_SOURCE';
static type = SOURCE_TYPES.ES_TERM_SOURCE;
constructor(descriptor, inspectorAdapters) {
super(descriptor, inspectorAdapters);

View file

@ -16,7 +16,7 @@ import { TileLayer } from '../../tile_layer';
import { getKibanaTileMap } from '../../../meta';
export const kibanaBasemapLayerWizardConfig: LayerWizard = {
checkVisibility: () => {
checkVisibility: async () => {
const tilemap = getKibanaTileMap();
return !!tilemap.url;
},

View file

@ -9,7 +9,6 @@ import { AbstractLayer } from './layer';
import { IVectorSource } from './sources/vector_source';
import {
MapFilters,
LayerDescriptor,
VectorLayerDescriptor,
VectorSourceRequestMeta,
} from '../../common/descriptor_types';
@ -35,7 +34,7 @@ export interface IVectorLayer extends ILayer {
export class VectorLayer extends AbstractLayer implements IVectorLayer {
protected readonly _style: IVectorStyle;
static createDescriptor(
options: Partial<LayerDescriptor>,
options: Partial<VectorLayerDescriptor>,
mapColors?: string[]
): VectorLayerDescriptor;