From c4c95e2bc64f5a9b919eb72af26eba1bc6879190 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 6 Dec 2019 09:34:52 -0700 Subject: [PATCH 01/26] [Maps] use style metadata to calculate symbolization bands (#51713) * [Maps] use style metadata to calculate symbolization bands * only update style meta when fields change * load join source style meta * use style meta data request to populate range * apply source filter to style meta request * fix heatmap * only use style meta range if field supports field meta * add fieldMetaOptions to style prperty descriptor and add migration script * add UI for setting fieldMetaOptions.isEnabled * clean up * review feedback * fix can_skip_fetch tests * review feedback * only show field meta popover for fields that support field meta * avoid duplicate fields re-fetching style meta * clean up problems when first creating grid source * update text for enabling field meta toggle * provide UI for setting sigma * allow users to include global time in style meta request * update SIEM saved objects * add less than and greater than symbols when styling by field stats * fix functional tests * review feedback * add support for date fields * review feedback * only show less then and greater then in legend when values will be outside of std range * unnest VectorStyle._getFieldRange * remove unused function * only show style isTimeAware switch when style fields use field meta --- .../legacy/plugins/maps/common/constants.js | 11 +- .../migrations/add_field_meta_options.js | 38 +++++ .../migrations/add_field_meta_options.test.js | 121 +++++++++++++++ x-pack/legacy/plugins/maps/migrations.js | 6 +- .../maps/public/layers/fields/es_agg_field.js | 17 ++- .../public/layers/fields/es_agg_field.test.js | 29 ++++ .../maps/public/layers/fields/es_doc_field.js | 27 ++++ .../maps/public/layers/fields/field.js | 8 + .../maps/public/layers/joins/inner_join.js | 7 +- .../plugins/maps/public/layers/layer.js | 2 +- .../es_geo_grid_source/es_geo_grid_source.js | 21 ++- .../update_source_editor.js | 4 +- .../es_pew_pew_source/es_pew_pew_source.js | 19 ++- .../es_search_source/es_search_source.js | 16 +- .../maps/public/layers/sources/es_source.js | 44 +++++- .../public/layers/sources/es_term_source.js | 10 +- .../maps/public/layers/sources/source.js | 6 +- .../layers/styles/heatmap/heatmap_style.js | 3 +- .../components/field_meta_options_popover.js | 139 ++++++++++++++++++ .../components/get_vector_style_label.js | 12 +- .../legend/style_property_legend_row.js | 12 +- .../components/static_dynamic_style_row.js | 32 +++- .../vector/components/vector_style_editor.js | 46 +++++- .../properties/dynamic_color_property.js | 2 +- .../dynamic_orientation_property.js | 4 +- .../properties/dynamic_size_property.js | 8 +- .../properties/dynamic_style_property.js | 23 ++- .../public/layers/styles/vector/style_util.js | 17 ++- .../layers/styles/vector/style_util.test.js | 33 +++++ .../layers/styles/vector/vector_style.js | 125 ++++++++++++---- .../styles/vector/vector_style_defaults.js | 45 ++++-- .../maps/public/layers/util/can_skip_fetch.js | 19 +++ .../public/layers/util/can_skip_fetch.test.js | 6 +- .../maps/public/layers/util/data_request.js | 2 +- .../public/layers/util/is_metric_countable.js | 11 ++ .../maps/public/layers/vector_layer.js | 94 +++++++++++- .../components/embeddables/__mocks__/mock.ts | 4 + .../components/embeddables/map_config.ts | 4 + .../es_archives/maps/kibana/data.json | 2 +- 39 files changed, 910 insertions(+), 119 deletions(-) create mode 100644 x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js create mode 100644 x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js create mode 100644 x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js diff --git a/x-pack/legacy/plugins/maps/common/constants.js b/x-pack/legacy/plugins/maps/common/constants.js index 3b2f887e13c87..77b57e3fe4965 100644 --- a/x-pack/legacy/plugins/maps/common/constants.js +++ b/x-pack/legacy/plugins/maps/common/constants.js @@ -57,6 +57,8 @@ export const FIELD_ORIGIN = { }; export const SOURCE_DATA_ID_ORIGIN = 'source'; +export const META_ID_ORIGIN_SUFFIX = 'meta'; +export const SOURCE_META_ID_ORIGIN = `${SOURCE_DATA_ID_ORIGIN}_${META_ID_ORIGIN_SUFFIX}`; export const GEOJSON_FILE = 'GEOJSON_FILE'; @@ -124,6 +126,11 @@ export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLab export const COUNT_PROP_NAME = 'doc_count'; export const STYLE_TYPE = { - 'STATIC': 'STATIC', - 'DYNAMIC': 'DYNAMIC' + STATIC: 'STATIC', + DYNAMIC: 'DYNAMIC' +}; + +export const LAYER_STYLE_TYPE = { + VECTOR: 'VECTOR', + HEATMAP: 'HEATMAP' }; diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js new file mode 100644 index 0000000000000..ed585e013d06f --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.js @@ -0,0 +1,38 @@ +/* + * 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 { LAYER_TYPE, STYLE_TYPE } from '../constants'; + +function isVectorLayer(layerDescriptor) { + const layerType = _.get(layerDescriptor, 'type'); + return layerType === LAYER_TYPE.VECTOR; +} + +export function addFieldMetaOptions({ attributes }) { + if (!attributes.layerListJSON) { + return attributes; + } + + const layerList = JSON.parse(attributes.layerListJSON); + layerList.forEach((layerDescriptor) => { + if (isVectorLayer(layerDescriptor) && _.has(layerDescriptor, 'style.properties')) { + Object.values(layerDescriptor.style.properties).forEach(stylePropertyDescriptor => { + if (stylePropertyDescriptor.type === STYLE_TYPE.DYNAMIC) { + stylePropertyDescriptor.options.fieldMetaOptions = { + isEnabled: false, // turn off field metadata to avoid changing behavior of existing saved objects + sigma: 3, + }; + } + }); + } + }); + + return { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }; +} diff --git a/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js new file mode 100644 index 0000000000000..905f77223b3bc --- /dev/null +++ b/x-pack/legacy/plugins/maps/common/migrations/add_field_meta_options.test.js @@ -0,0 +1,121 @@ +/* + * 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 { addFieldMetaOptions } from './add_field_meta_options'; +import { LAYER_TYPE, STYLE_TYPE } from '../constants'; + +describe('addFieldMetaOptions', () => { + + test('Should handle missing layerListJSON attribute', () => { + const attributes = { + title: 'my map', + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + }); + }); + + test('Should ignore non-vector layers', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.HEATMAP, + style: { + type: 'HEATMAP', + colorRampName: 'Greens' + } + } + ]); + const attributes = { + title: 'my map', + layerListJSON + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + layerListJSON + }); + }); + + test('Should ignore static style properties', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + style: { + type: 'VECTOR', + properties: { + lineColor: { + type: STYLE_TYPE.STATIC, + options: { + color: '#FFFFFF' + } + } + } + } + } + ]); + const attributes = { + title: 'my map', + layerListJSON + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + layerListJSON + }); + }); + + test('Should add field meta options to dynamic style properties', () => { + const layerListJSON = JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + field: { + name: 'my_field', + origin: 'source' + }, + color: 'Greys' + } + } + } + } + } + ]); + const attributes = { + title: 'my map', + layerListJSON + }; + expect(addFieldMetaOptions({ attributes })).toEqual({ + title: 'my map', + layerListJSON: JSON.stringify([ + { + type: LAYER_TYPE.VECTOR, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: STYLE_TYPE.DYNAMIC, + options: { + field: { + name: 'my_field', + origin: 'source' + }, + color: 'Greys', + fieldMetaOptions: { + isEnabled: false, + sigma: 3, + } + } + } + } + } + } + ]) + }); + }); +}); diff --git a/x-pack/legacy/plugins/maps/migrations.js b/x-pack/legacy/plugins/maps/migrations.js index 39dc58f259961..df19c8425199a 100644 --- a/x-pack/legacy/plugins/maps/migrations.js +++ b/x-pack/legacy/plugins/maps/migrations.js @@ -8,6 +8,7 @@ import { extractReferences } from './common/migrations/references'; import { emsRasterTileToEmsVectorTile } from './common/migrations/ems_raster_tile_to_ems_vector_tile'; import { topHitsTimeToSort } from './common/migrations/top_hits_time_to_sort'; import { moveApplyGlobalQueryToSources } from './common/migrations/move_apply_global_query'; +import { addFieldMetaOptions } from './common/migrations/add_field_meta_options'; export const migrations = { 'map': { @@ -37,11 +38,12 @@ export const migrations = { }; }, '7.6.0': (doc) => { - const attributes = moveApplyGlobalQueryToSources(doc); + const attributesPhase1 = moveApplyGlobalQueryToSources(doc); + const attributesPhase2 = addFieldMetaOptions({ attributes: attributesPhase1 }); return { ...doc, - attributes, + attributes: attributesPhase2, }; } }, diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js index eb80169e94eab..af78e3a871802 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.js @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ - import { AbstractField } from './field'; import { COUNT_AGG_TYPE } from '../../../common/constants'; +import { isMetricCountable } from '../util/is_metric_countable'; import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_property'; export class ESAggMetricField extends AbstractField { @@ -36,6 +36,11 @@ export class ESAggMetricField extends AbstractField { return (this.getAggType() === COUNT_AGG_TYPE) ? true : !!this._esDocField; } + async getDataType() { + // aggregations only provide numerical data + return 'number'; + } + getESDocFieldName() { return this._esDocField ? this._esDocField.getName() : ''; } @@ -55,7 +60,6 @@ export class ESAggMetricField extends AbstractField { ); } - makeMetricAggConfig() { const metricAggConfig = { id: this.getName(), @@ -69,4 +73,13 @@ export class ESAggMetricField extends AbstractField { } return metricAggConfig; } + + supportsFieldMeta() { + // count and sum aggregations are not within field bounds so they do not support field meta. + return !isMetricCountable(this.getAggType()); + } + + async getFieldMetaRequest(config) { + return this._esDocField.getFieldMetaRequest(config); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js new file mode 100644 index 0000000000000..65b8c518fa895 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_agg_field.test.js @@ -0,0 +1,29 @@ +/* + * 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 { ESAggMetricField } from './es_agg_field'; +import { METRIC_TYPE } from '../../../common/constants'; + +describe('supportsFieldMeta', () => { + + test('Non-counting aggregations should support field meta', () => { + const avgMetric = new ESAggMetricField({ aggType: METRIC_TYPE.AVG }); + expect(avgMetric.supportsFieldMeta()).toBe(true); + const maxMetric = new ESAggMetricField({ aggType: METRIC_TYPE.MAX }); + expect(maxMetric.supportsFieldMeta()).toBe(true); + const minMetric = new ESAggMetricField({ aggType: METRIC_TYPE.MIN }); + expect(minMetric.supportsFieldMeta()).toBe(true); + }); + + test('Counting aggregations should not support field meta', () => { + const countMetric = new ESAggMetricField({ aggType: METRIC_TYPE.COUNT }); + expect(countMetric.supportsFieldMeta()).toBe(false); + const sumMetric = new ESAggMetricField({ aggType: METRIC_TYPE.SUM }); + expect(sumMetric.supportsFieldMeta()).toBe(false); + const uniqueCountMetric = new ESAggMetricField({ aggType: METRIC_TYPE.UNIQUE_COUNT }); + expect(uniqueCountMetric.supportsFieldMeta()).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js index 5cc0c9a29ce02..ad15c6249e554 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/es_doc_field.js @@ -27,4 +27,31 @@ export class ESDocField extends AbstractField { return field.type; } + supportsFieldMeta() { + return true; + } + + async getFieldMetaRequest(/* config */) { + const field = await this._getField(); + + if (field.type !== 'number' && field.type !== 'date') { + return null; + } + + const extendedStats = {}; + if (field.scripted) { + extendedStats.script = { + source: field.script, + lang: field.lang + }; + } else { + extendedStats.field = this._fieldName; + } + return { + [this._fieldName]: { + extended_stats: extendedStats + } + }; + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/fields/field.js b/x-pack/legacy/plugins/maps/public/layers/fields/field.js index b53c6991c6ebe..f1bb116d29c8b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/fields/field.js +++ b/x-pack/legacy/plugins/maps/public/layers/fields/field.js @@ -42,4 +42,12 @@ export class AbstractField { getOrigin() { return this._origin; } + + supportsFieldMeta() { + return false; + } + + async getFieldMetaRequest(/* config */) { + return null; + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js index 184fdc0663bd7..432492973cce0 100644 --- a/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js +++ b/x-pack/legacy/plugins/maps/public/layers/joins/inner_join.js @@ -7,6 +7,7 @@ import { ESTermSource } from '../sources/es_term_source'; import { getComputedFieldNamePrefix } from '../styles/vector/style_util'; +import { META_ID_ORIGIN_SUFFIX } from '../../../common/constants'; export class InnerJoin { @@ -36,10 +37,14 @@ export class InnerJoin { // Source request id must be static and unique because the re-fetch logic uses the id to locate the previous request. // Elasticsearch sources have a static and unique id so that requests can be modified in the inspector. // Using the right source id as the source request id because it meets the above criteria. - getSourceId() { + getSourceDataRequestId() { return `join_source_${this._rightSource.getId()}`; } + getSourceMetaDataRequestId() { + return `${this.getSourceDataRequestId()}_${META_ID_ORIGIN_SUFFIX}`; + } + getLeftField() { return this._leftField; } diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 1c2f33df66bf8..b1f3c32f267b9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -80,7 +80,7 @@ export class AbstractLayer { } supportsElasticsearchFilters() { - return this._source.supportsElasticsearchFilters(); + return this._source.isESSource(); } async supportsFitToBounds() { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index 413f99480a8c2..f4cb43ad90146 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -15,7 +15,7 @@ import { AggConfigs } from 'ui/agg_types'; import { tabifyAggResponse } from 'ui/agg_response/tabify'; import { convertToGeoJson } from './convert_to_geojson'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { vectorStyles } from '../../styles/vector/vector_style_defaults'; +import { getDefaultDynamicProperties, VECTOR_STYLES } from '../../styles/vector/vector_style_defaults'; import { RENDER_AS } from './render_as'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; @@ -170,13 +170,15 @@ export class ESGeoGridSource extends AbstractESAggSource { const searchSource = await this._makeSearchSource(searchFilters, 0); const aggConfigs = new AggConfigs(indexPattern, this._makeAggConfigs(searchFilters.geogridPrecision), aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); - const esResponse = await this._runEsQuery( - layerName, + const esResponse = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, searchSource, registerCancelCallback, - i18n.translate('xpack.maps.source.esGrid.inspectorDescription', { + requestDescription: i18n.translate('xpack.maps.source.esGrid.inspectorDescription', { defaultMessage: 'Elasticsearch geo grid aggregation request' - })); + }), + }); const tabifiedResp = tabifyAggResponse(aggConfigs, esResponse); const { featureCollection } = convertToGeoJson({ @@ -226,10 +228,14 @@ export class ESGeoGridSource extends AbstractESAggSource { sourceDescriptor: this._descriptor, ...options }); + + const defaultDynamicProperties = getDefaultDynamicProperties(); + descriptor.style = VectorStyle.createDescriptor({ - [vectorStyles.FILL_COLOR]: { + [VECTOR_STYLES.FILL_COLOR]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, @@ -238,9 +244,10 @@ export class ESGeoGridSource extends AbstractESAggSource { color: 'Blues' } }, - [vectorStyles.ICON_SIZE]: { + [VECTOR_STYLES.ICON_SIZE]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js index 1b446e1f2159a..cc1e53dc5cb3f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js @@ -8,13 +8,13 @@ import React, { Fragment, Component } from 'react'; import { RENDER_AS } from './render_as'; import { MetricsEditor } from '../../../components/metrics_editor'; -import { METRIC_TYPE } from '../../../../common/constants'; import { indexPatternService } from '../../../kibana_services'; import { ResolutionEditor } from './resolution_editor'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; +import { isMetricCountable } from '../../util/is_metric_countable'; export class UpdateSourceEditor extends Component { state = { @@ -72,7 +72,7 @@ export class UpdateSourceEditor extends Component { this.props.renderAs === RENDER_AS.HEATMAP ? metric => { //these are countable metrics, where blending heatmap color blobs make sense - return [METRIC_TYPE.COUNT, METRIC_TYPE.SUM, METRIC_TYPE.UNIQUE_COUNT].includes(metric.value); + return isMetricCountable(metric.value); } : null; const allowMultipleMetrics = this.props.renderAs !== RENDER_AS.HEATMAP; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 01220136b14f3..4eb0a952defba 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -12,7 +12,7 @@ import { VectorLayer } from '../../vector_layer'; import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { VectorStyle } from '../../styles/vector/vector_style'; -import { vectorStyles } from '../../styles/vector/vector_style_defaults'; +import { getDefaultDynamicProperties, VECTOR_STYLES } from '../../styles/vector/vector_style_defaults'; import { i18n } from '@kbn/i18n'; import { SOURCE_DATA_ID_ORIGIN, ES_PEW_PEW, COUNT_PROP_NAME, COUNT_PROP_LABEL } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -123,10 +123,12 @@ export class ESPewPewSource extends AbstractESAggSource { } createDefaultLayer(options) { + const defaultDynamicProperties = getDefaultDynamicProperties(); const styleDescriptor = VectorStyle.createDescriptor({ - [vectorStyles.LINE_COLOR]: { + [VECTOR_STYLES.LINE_COLOR]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, @@ -135,9 +137,10 @@ export class ESPewPewSource extends AbstractESAggSource { color: 'Blues' } }, - [vectorStyles.LINE_WIDTH]: { + [VECTOR_STYLES.LINE_WIDTH]: { type: DynamicStyleProperty.type, options: { + ...defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH].options, field: { label: COUNT_PROP_LABEL, name: COUNT_PROP_NAME, @@ -203,13 +206,15 @@ export class ESPewPewSource extends AbstractESAggSource { } }); - const esResponse = await this._runEsQuery( - layerName, + const esResponse = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, searchSource, registerCancelCallback, - i18n.translate('xpack.maps.source.pewPew.inspectorDescription', { + requestDescription: i18n.translate('xpack.maps.source.pewPew.inspectorDescription', { defaultMessage: 'Source-destination connections request' - })); + }), + }); const { featureCollection } = convertToLines(esResponse); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 57a43f924b7e6..453a1851e47aa 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -261,7 +261,13 @@ export class ESSearchSource extends AbstractESSource { } }); - const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document top hits request'); + const resp = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, + searchSource, + registerCancelCallback, + requestDescription: 'Elasticsearch document top hits request', + }); const allHits = []; const entityBuckets = _.get(resp, 'aggregations.entitySplit.buckets', []); @@ -322,7 +328,13 @@ export class ESSearchSource extends AbstractESSource { searchSource.setField('sort', this._buildEsSort()); } - const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document request'); + const resp = await this._runEsQuery({ + requestId: this.getId(), + requestName: layerName, + searchSource, + registerCancelCallback, + requestDescription: 'Elasticsearch document request', + }); return { hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index c2f4f7e755288..b5d7f7a6f606a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -54,7 +54,7 @@ export class AbstractESSource extends AbstractVectorSource { return []; } - supportsElasticsearchFilters() { + isESSource() { return true; } @@ -73,7 +73,7 @@ export class AbstractESSource extends AbstractVectorSource { return []; } - async _runEsQuery(requestName, searchSource, registerCancelCallback, requestDescription) { + async _runEsQuery({ requestId, requestName, requestDescription, searchSource, registerCancelCallback }) { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); @@ -82,7 +82,7 @@ export class AbstractESSource extends AbstractVectorSource { inspectorAdapters: this._inspectorAdapters, searchSource, requestName, - requestId: this.getId(), + requestId, requestDesc: requestDescription, abortSignal: abortController.signal, }); @@ -271,4 +271,42 @@ export class AbstractESSource extends AbstractVectorSource { return fieldFromIndexPattern.format.getConverterFor('text'); } + async loadStylePropsMeta(layerName, style, dynamicStyleProps, registerCancelCallback, searchFilters) { + const promises = dynamicStyleProps.map(dynamicStyleProp => { + return dynamicStyleProp.getFieldMetaRequest(); + }); + + const fieldAggRequests = await Promise.all(promises); + const aggs = fieldAggRequests.reduce((aggs, fieldAggRequest) => { + return fieldAggRequest ? { ...aggs, ...fieldAggRequest } : aggs; + }, {}); + + const indexPattern = await this.getIndexPattern(); + const searchSource = new SearchSource(); + searchSource.setField('index', indexPattern); + searchSource.setField('size', 0); + searchSource.setField('aggs', aggs); + if (searchFilters.sourceQuery) { + searchSource.setField('query', searchFilters.sourceQuery); + } + if (style.isTimeAware() && await this.isTimeAware()) { + searchSource.setField('filter', [timefilter.createFilter(indexPattern, searchFilters.timeFilters)]); + } + + const resp = await this._runEsQuery({ + requestId: `${this.getId()}_styleMeta`, + requestName: i18n.translate('xpack.maps.source.esSource.stylePropsMetaRequestName', { + defaultMessage: '{layerName} - metadata', + values: { layerName } + }), + searchSource, + registerCancelCallback, + requestDescription: i18n.translate('xpack.maps.source.esSource.stylePropsMetaRequestDescription', { + defaultMessage: 'Elasticsearch request retrieving field metadata used for calculating symbolization bands.', + }), + }); + + return resp.aggregations; + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index afc402fa81bcb..57366e502d581 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -103,9 +103,13 @@ export class ESTermSource extends AbstractESAggSource { const aggConfigs = new AggConfigs(indexPattern, configStates, aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); - const requestName = `${this._descriptor.indexPatternTitle}.${this._termField.getName()}`; - const requestDesc = this._getRequestDescription(leftSourceName, leftFieldName); - const rawEsData = await this._runEsQuery(requestName, searchSource, registerCancelCallback, requestDesc); + const rawEsData = await this._runEsQuery({ + requestId: this.getId(), + requestName: `${this._descriptor.indexPatternTitle}.${this._termField.getName()}`, + searchSource, + registerCancelCallback, + requestDescription: this._getRequestDescription(leftSourceName, leftFieldName), + }); const metricPropertyNames = configStates .filter(configState => { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/source.js b/x-pack/legacy/plugins/maps/public/layers/sources/source.js index 78e57f79bbe56..d3b2971dbbb0c 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/source.js @@ -123,7 +123,7 @@ export class AbstractSource { return AbstractSource.isIndexingSource; } - supportsElasticsearchFilters() { + isESSource() { return false; } @@ -136,6 +136,10 @@ export class AbstractSource { async getFieldFormatter(/* fieldName */) { return null; } + + async loadStylePropsMeta() { + throw new Error(`Source#loadStylePropsMeta not implemented`); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index e4982c86b53bb..ed64f408b2585 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -10,13 +10,14 @@ import { AbstractStyle } from '../abstract_style'; import { HeatmapStyleEditor } from './components/heatmap_style_editor'; import { HeatmapLegend } from './components/legend/heatmap_legend'; import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants'; +import { LAYER_STYLE_TYPE } from '../../../../common/constants'; import { getColorRampStops } from '../color_utils'; import { i18n } from '@kbn/i18n'; import { EuiIcon } from '@elastic/eui'; export class HeatmapStyle extends AbstractStyle { - static type = 'HEATMAP'; + static type = LAYER_STYLE_TYPE.HEATMAP; constructor(descriptor = {}) { super(); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js new file mode 100644 index 0000000000000..095740abe3dda --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/field_meta_options_popover.js @@ -0,0 +1,139 @@ +/* + * 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 { + EuiButtonIcon, + EuiFormRow, + EuiPopover, + EuiRange, + EuiSwitch, +} from '@elastic/eui'; +import { VECTOR_STYLES } from '../vector_style_defaults'; +import { i18n } from '@kbn/i18n'; + +function getIsEnableToggleLabel(styleName) { + switch (styleName) { + case VECTOR_STYLES.FILL_COLOR: + case VECTOR_STYLES.LINE_COLOR: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.colorLabel', { + defaultMessage: 'Calculate color ramp range from indices' + }); + case VECTOR_STYLES.LINE_WIDTH: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.widthLabel', { + defaultMessage: 'Calculate border width range from indices' + }); + case VECTOR_STYLES.ICON_SIZE: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.sizeLabel', { + defaultMessage: 'Calculate symbol size range from indices' + }); + default: + return i18n.translate('xpack.maps.styles.fieldMetaOptions.isEnabled.defaultLabel', { + defaultMessage: 'Calculate symbolization range from indices' + }); + } +} + +export class FieldMetaOptionsPopover extends Component { + + state = { + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + } + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + } + + _onIsEnabledChange = event => { + this.props.onChange({ + ...this.props.styleProperty.getFieldMetaOptions(), + isEnabled: event.target.checked, + }); + }; + + _onSigmaChange = event => { + this.props.onChange({ + ...this.props.styleProperty.getFieldMetaOptions(), + sigma: event.target.value, + }); + } + + _renderButton() { + return ( + + ); + } + + _renderContent() { + return ( + + + + + + + + + + ); + } + + render() { + if (!this.props.styleProperty.supportsFieldMeta()) { + return null; + } + + return ( + + {this._renderContent()} + + ); + } +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js index 0984b0189558d..b21577d214bb5 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js @@ -6,27 +6,27 @@ import { i18n } from '@kbn/i18n'; -import { vectorStyles } from '../vector_style_defaults'; +import { VECTOR_STYLES } from '../vector_style_defaults'; export function getVectorStyleLabel(styleName) { switch (styleName) { - case vectorStyles.FILL_COLOR: + case VECTOR_STYLES.FILL_COLOR: return i18n.translate('xpack.maps.styles.vector.fillColorLabel', { defaultMessage: 'Fill color' }); - case vectorStyles.LINE_COLOR: + case VECTOR_STYLES.LINE_COLOR: return i18n.translate('xpack.maps.styles.vector.borderColorLabel', { defaultMessage: 'Border color' }); - case vectorStyles.LINE_WIDTH: + case VECTOR_STYLES.LINE_WIDTH: return i18n.translate('xpack.maps.styles.vector.borderWidthLabel', { defaultMessage: 'Border width' }); - case vectorStyles.ICON_SIZE: + case VECTOR_STYLES.ICON_SIZE: return i18n.translate('xpack.maps.styles.vector.symbolSizeLabel', { defaultMessage: 'Symbol size' }); - case vectorStyles.ICON_ORIENTATION: + case VECTOR_STYLES.ICON_ORIENTATION: return i18n.translate('xpack.maps.styles.vector.orientationLabel', { defaultMessage: 'Symbol orientation' }); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js index 35c7066b7fd0f..dc5098c4d6d4d 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js @@ -81,18 +81,24 @@ export class StylePropertyLegendRow extends Component { } render() { - const { range, style } = this.props; if (this._excludeFromHeader()) { return null; } const header = style.renderHeader(); + + const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE)); + const minLabel = this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange ? `< ${min}` : min; + + const max = this._formatValue(_.get(range, 'max', EMPTY_VALUE)); + const maxLabel = this.props.style.isFieldMetaEnabled() && range && range.isMaxOutsideStdRange ? `> ${max}` : max; + return ( diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js index d1de8e0fe6b4a..9686214fec9fe 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/static_dynamic_style_row.js @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Component, Fragment } from 'react'; import { VectorStyle } from '../vector_style'; import { i18n } from '@kbn/i18n'; +import { FieldMetaOptionsPopover } from './field_meta_options_popover'; import { getVectorStyleLabel } from './get_vector_style_label'; import { EuiFlexGroup, EuiFlexItem, EuiToolTip, EuiFormRow, EuiButtonToggle } from '@elastic/eui'; -export class StaticDynamicStyleRow extends React.Component { +export class StaticDynamicStyleRow extends Component { // Store previous options locally so when type is toggled, // previous style options can be used. prevStaticStyleOptions = this.props.defaultStaticStyleOptions; @@ -29,6 +30,17 @@ export class StaticDynamicStyleRow extends React.Component { return this.props.styleProperty.getOptions(); } + _onFieldMetaOptionsChange = fieldMetaOptions => { + const styleDescriptor = { + type: VectorStyle.STYLE_TYPE.DYNAMIC, + options: { + ...this._getStyleOptions(), + fieldMetaOptions + } + }; + this.props.handlePropertyChange(this.props.styleProperty.getStyleName(), styleDescriptor); + } + _onStaticStyleChange = options => { const styleDescriptor = { type: VectorStyle.STYLE_TYPE.STATIC, @@ -64,11 +76,17 @@ export class StaticDynamicStyleRow extends React.Component { if (this._isDynamic()) { const DynamicSelector = this.props.DynamicSelector; return ( - + + + + ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js index 3043d57c04037..d848b9274d071 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js @@ -22,7 +22,7 @@ import { SYMBOLIZE_AS_ICON } from '../vector_constants'; import { i18n } from '@kbn/i18n'; import { SYMBOL_OPTIONS } from '../symbol_utils'; -import { EuiSpacer, EuiButtonGroup } from '@elastic/eui'; +import { EuiSpacer, EuiButtonGroup, EuiFormRow, EuiSwitch } from '@elastic/eui'; export class VectorStyleEditor extends Component { state = { @@ -117,6 +117,14 @@ export class VectorStyleEditor extends Component { return [...this.state.dateFields, ...this.state.numberFields]; } + _handleSelectedFeatureChange = selectedFeature => { + this.setState({ selectedFeature }); + }; + + _onIsTimeAwareChange = event => { + this.props.onIsTimeAwareChange(event.target.checked); + }; + _renderFillColor() { return ( { - this.setState({ selectedFeature }); - }; - - render() { + _renderProperties() { const { supportedFeatures, selectedFeature } = this.state; if (!supportedFeatures) { @@ -302,4 +306,34 @@ export class VectorStyleEditor extends Component { ); } + + _renderIsTimeAwareSwitch() { + if (!this.props.showIsTimeAware) { + return null; + } + + return ( + + + + ); + } + + render() { + return ( + + {this._renderProperties()} + {this._renderIsTimeAwareSwitch()} + + ); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js index 4b4b853c274cb..d56db31d17067 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js @@ -50,7 +50,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { } isCustomColorRamp() { - return !!this._options.customColorRamp; + return this._options.useCustomColorRamp; } supportsFeatureState() { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js index fb4ffd8cce4b4..afbe924e1afb8 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js @@ -7,14 +7,14 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { getComputedFieldName } from '../style_util'; -import { vectorStyles } from '../vector_style_defaults'; +import { VECTOR_STYLES } from '../vector_style_defaults'; export class DynamicOrientationProperty extends DynamicStyleProperty { syncIconRotationWithMb(symbolLayerId, mbMap) { if (this._options.field && this._options.field.name) { - const targetName = getComputedFieldName(vectorStyles.ICON_ORIENTATION, this._options.field.name); + const targetName = getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this._options.field.name); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]); } else { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js index bd011b27d81c8..b4e6cf7be1701 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js @@ -8,7 +8,7 @@ import { DynamicStyleProperty } from './dynamic_style_property'; import { getComputedFieldName } from '../style_util'; import { HALF_LARGE_MAKI_ICON_SIZE, LARGE_MAKI_ICON_SIZE, SMALL_MAKI_ICON_SIZE } from '../symbol_utils'; -import { vectorStyles } from '../vector_style_defaults'; +import { VECTOR_STYLES } from '../vector_style_defaults'; import _ from 'lodash'; import { CircleIcon } from '../components/legend/circle_icon'; import React, { Fragment } from 'react'; @@ -55,7 +55,7 @@ export class DynamicSizeProperty extends DynamicStyleProperty { mbMap.setLayoutProperty(symbolLayerId, 'icon-image', `${symbolId}-${iconPixels}`); const halfIconPixels = iconPixels / 2; - const targetName = getComputedFieldName(vectorStyles.ICON_SIZE, this._options.field.name); + const targetName = getComputedFieldName(VECTOR_STYLES.ICON_SIZE, this._options.field.name); // Using property state instead of feature-state because layout properties do not support feature-state mbMap.setLayoutProperty(symbolLayerId, 'icon-size', [ 'interpolate', @@ -112,9 +112,9 @@ export class DynamicSizeProperty extends DynamicStyleProperty { renderHeader() { let icons; - if (this.getStyleName() === vectorStyles.LINE_WIDTH) { + if (this.getStyleName() === VECTOR_STYLES.LINE_WIDTH) { icons = getLineWidthIcons(); - } else if (this.getStyleName() === vectorStyles.ICON_SIZE) { + } else if (this.getStyleName() === VECTOR_STYLES.ICON_SIZE) { icons = getSymbolSizeIcons(); } else { return null; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index e87bcc12c99be..a72502f9f17fb 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ - +import _ from 'lodash'; import { AbstractStyleProperty } from './style_property'; +import { DEFAULT_SIGMA } from '../vector_style_defaults'; import { STYLE_TYPE } from '../../../../../common/constants'; export class DynamicStyleProperty extends AbstractStyleProperty { @@ -32,6 +33,22 @@ export class DynamicStyleProperty extends AbstractStyleProperty { return this._field.getOrigin(); } + isFieldMetaEnabled() { + const fieldMetaOptions = this.getFieldMetaOptions(); + return this.supportsFieldMeta() && _.get(fieldMetaOptions, 'isEnabled', true); + } + + supportsFieldMeta() { + return this.isComplete() && this.isScaled() && this._field.supportsFieldMeta(); + } + + async getFieldMetaRequest() { + const fieldMetaOptions = this.getFieldMetaOptions(); + return this._field.getFieldMetaRequest({ + sigma: _.get(fieldMetaOptions, 'sigma', DEFAULT_SIGMA), + }); + } + supportsFeatureState() { return true; } @@ -39,4 +56,8 @@ export class DynamicStyleProperty extends AbstractStyleProperty { isScaled() { return true; } + + getFieldMetaOptions() { + return _.get(this.getOptions(), 'fieldMetaOptions', {}); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js index 69caaca080138..699955fe6542a 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ - export function getComputedFieldName(styleName, fieldName) { return `${getComputedFieldNamePrefix(fieldName)}__${styleName}`; } @@ -12,3 +11,19 @@ export function getComputedFieldName(styleName, fieldName) { export function getComputedFieldNamePrefix(fieldName) { return `__kbn__dynamic__${fieldName}`; } + +export function scaleValue(value, range) { + if (isNaN(value) || !range) { + return -1; //Nothing to scale, put outside scaled range + } + + if (range.delta === 0 || value >= range.max) { + return 1; //snap to end of scaled range + } + + if (value <= range.min) { + return 0; //snap to beginning of scaled range + } + + return (value - range.min) / range.delta; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js new file mode 100644 index 0000000000000..a25e3bf8684c9 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/style_util.test.js @@ -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 { scaleValue } from './style_util'; + +describe('scaleValue', () => { + test('Should scale value between 0 and 1', () => { + expect(scaleValue(5, { min: 0, max: 10, delta: 10 })).toBe(0.5); + }); + + test('Should snap value less then range min to 0', () => { + expect(scaleValue(-1, { min: 0, max: 10, delta: 10 })).toBe(0); + }); + + test('Should snap value greater then range max to 1', () => { + expect(scaleValue(11, { min: 0, max: 10, delta: 10 })).toBe(1); + }); + + test('Should snap value to 1 when tere is not range delta', () => { + expect(scaleValue(10, { min: 10, max: 10, delta: 0 })).toBe(1); + }); + + test('Should put value as -1 when value is not provided', () => { + expect(scaleValue(undefined, { min: 0, max: 10, delta: 10 })).toBe(-1); + }); + + test('Should put value as -1 when range is not provided', () => { + expect(scaleValue(5, undefined)).toBe(-1); + }); +}); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 45a1636e5c033..53794f2043aad 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -7,15 +7,21 @@ import _ from 'lodash'; import React from 'react'; import { VectorStyleEditor } from './components/vector_style_editor'; -import { getDefaultProperties, vectorStyles } from './vector_style_defaults'; +import { getDefaultProperties, VECTOR_STYLES } from './vector_style_defaults'; import { AbstractStyle } from '../abstract_style'; -import { GEO_JSON_TYPE, FIELD_ORIGIN, STYLE_TYPE } from '../../../../common/constants'; +import { + GEO_JSON_TYPE, + FIELD_ORIGIN, + STYLE_TYPE, + SOURCE_META_ID_ORIGIN, + LAYER_STYLE_TYPE, +} from '../../../../common/constants'; import { VectorIcon } from './components/legend/vector_icon'; import { VectorStyleLegend } from './components/legend/vector_style_legend'; import { VECTOR_SHAPE_TYPES } from '../../sources/vector_feature_types'; import { SYMBOLIZE_AS_CIRCLE, SYMBOLIZE_AS_ICON } from './vector_constants'; import { getMakiSymbolAnchor } from './symbol_utils'; -import { getComputedFieldName } from './style_util'; +import { getComputedFieldName, scaleValue } from './style_util'; import { StaticStyleProperty } from './properties/static_style_property'; import { DynamicStyleProperty } from './properties/dynamic_style_property'; import { DynamicSizeProperty } from './properties/dynamic_size_property'; @@ -31,12 +37,13 @@ const POLYGONS = [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON]; export class VectorStyle extends AbstractStyle { - static type = 'VECTOR'; + static type = LAYER_STYLE_TYPE.VECTOR; static STYLE_TYPE = STYLE_TYPE; - static createDescriptor(properties = {}) { + static createDescriptor(properties = {}, isTimeAware = true) { return { type: VectorStyle.type, - properties: { ...getDefaultProperties(), ...properties } + properties: { ...getDefaultProperties(), ...properties }, + isTimeAware, }; } @@ -50,15 +57,15 @@ export class VectorStyle extends AbstractStyle { this._layer = layer; this._descriptor = { ...descriptor, - ...VectorStyle.createDescriptor(descriptor.properties), + ...VectorStyle.createDescriptor(descriptor.properties, descriptor.isTimeAware), }; - this._lineColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.LINE_COLOR], vectorStyles.LINE_COLOR); - this._fillColorStyleProperty = this._makeColorProperty(this._descriptor.properties[vectorStyles.FILL_COLOR], vectorStyles.FILL_COLOR); - this._lineWidthStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.LINE_WIDTH], vectorStyles.LINE_WIDTH); - this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[vectorStyles.ICON_SIZE], vectorStyles.ICON_SIZE); + this._lineColorStyleProperty = this._makeColorProperty(this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], VECTOR_STYLES.LINE_COLOR); + this._fillColorStyleProperty = this._makeColorProperty(this._descriptor.properties[VECTOR_STYLES.FILL_COLOR], VECTOR_STYLES.FILL_COLOR); + this._lineWidthStyleProperty = this._makeSizeProperty(this._descriptor.properties[VECTOR_STYLES.LINE_WIDTH], VECTOR_STYLES.LINE_WIDTH); + this._iconSizeStyleProperty = this._makeSizeProperty(this._descriptor.properties[VECTOR_STYLES.ICON_SIZE], VECTOR_STYLES.ICON_SIZE); // eslint-disable-next-line max-len - this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[vectorStyles.ICON_ORIENTATION], vectorStyles.ICON_ORIENTATION); + this._iconOrientationProperty = this._makeOrientationProperty(this._descriptor.properties[VECTOR_STYLES.ICON_ORIENTATION], VECTOR_STYLES.ICON_ORIENTATION); } _getAllStyleProperties() { @@ -72,13 +79,22 @@ export class VectorStyle extends AbstractStyle { } renderEditor({ layer, onStyleDescriptorChange }) { - const styleProperties = { ...this.getRawProperties() }; + const rawProperties = this.getRawProperties(); const handlePropertyChange = (propertyName, settings) => { - styleProperties[propertyName] = settings;//override single property, but preserve the rest - const vectorStyleDescriptor = VectorStyle.createDescriptor(styleProperties); + rawProperties[propertyName] = settings;//override single property, but preserve the rest + const vectorStyleDescriptor = VectorStyle.createDescriptor(rawProperties, this.isTimeAware()); onStyleDescriptorChange(vectorStyleDescriptor); }; + const onIsTimeAwareChange = isTimeAware => { + const vectorStyleDescriptor = VectorStyle.createDescriptor(rawProperties, isTimeAware); + onStyleDescriptorChange(vectorStyleDescriptor); + }; + + const propertiesWithFieldMeta = this.getDynamicPropertiesArray().filter(dynamicStyleProp => { + return dynamicStyleProp.isFieldMetaEnabled(); + }); + return ( 0} /> ); } @@ -156,7 +175,7 @@ export class VectorStyle extends AbstractStyle { nextStyleDescriptor: VectorStyle.createDescriptor({ ...originalProperties, ...updatedProperties, - }) + }, this.isTimeAware()) }; } @@ -239,6 +258,10 @@ export class VectorStyle extends AbstractStyle { return fieldNames; } + isTimeAware() { + return this._descriptor.isTimeAware; + } + getRawProperties() { return this._descriptor.properties || {}; } @@ -277,7 +300,56 @@ export class VectorStyle extends AbstractStyle { } _getFieldRange = (fieldName) => { - return _.get(this._descriptor, ['__styleMeta', fieldName]); + const fieldRangeFromLocalFeatures = _.get(this._descriptor, ['__styleMeta', fieldName]); + const dynamicProps = this.getDynamicPropertiesArray(); + const dynamicProp = dynamicProps.find(dynamicProp => { return fieldName === dynamicProp.getField().getName(); }); + + if (!dynamicProp || !dynamicProp.isFieldMetaEnabled()) { + return fieldRangeFromLocalFeatures; + } + + let dataRequestId; + if (dynamicProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE) { + dataRequestId = SOURCE_META_ID_ORIGIN; + } else { + const join = this._layer.getValidJoins().find(join => { + const matchingField = join.getRightJoinSource().getMetricFieldForName(fieldName); + return !!matchingField; + }); + if (join) { + dataRequestId = join.getSourceMetaDataRequestId(); + } + } + + if (!dataRequestId) { + return fieldRangeFromLocalFeatures; + } + + const styleMetaDataRequest = this._layer._findDataRequestForSource(dataRequestId); + if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { + return fieldRangeFromLocalFeatures; + } + + const data = styleMetaDataRequest.getData(); + const field = dynamicProp.getField(); + const realFieldName = field.getESDocFieldName ? field.getESDocFieldName() : field.getName(); + const stats = data[realFieldName]; + if (!stats) { + return fieldRangeFromLocalFeatures; + } + + const sigma = _.get(dynamicProp.getFieldMetaOptions(), 'sigma', 3); + const stdLowerBounds = stats.avg - (stats.std_deviation * sigma); + const stdUpperBounds = stats.avg + (stats.std_deviation * sigma); + const min = Math.max(stats.min, stdLowerBounds); + const max = Math.min(stats.max, stdUpperBounds); + return { + min, + max, + delta: max - min, + isMinOutsideStdRange: stats.min < stdLowerBounds, + isMaxOutsideStdRange: stats.max > stdUpperBounds, + }; } getIcon = () => { @@ -289,8 +361,8 @@ export class VectorStyle extends AbstractStyle { ); @@ -321,7 +393,7 @@ export class VectorStyle extends AbstractStyle { // To work around this limitation, some styling values must fall back to geojson property values. let supportsFeatureState; let isScaled; - if (styleProperty.getStyleName() === vectorStyles.ICON_SIZE + if (styleProperty.getStyleName() === VECTOR_STYLES.ICON_SIZE && this._descriptor.properties.symbol.options.symbolizeAs === SYMBOLIZE_AS_ICON) { supportsFeatureState = false; isScaled = true; @@ -380,13 +452,7 @@ export class VectorStyle extends AbstractStyle { const value = parseFloat(feature.properties[name]); let styleValue; if (isScaled) { - if (isNaN(value) || !range) {//cannot scale - styleValue = -1;//put outside range - } else if (range.delta === 0) {//values are identical - styleValue = 1;//snap to end of color range - } else { - styleValue = (value - range.min) / range.delta; - } + styleValue = scaleValue(value, range); } else { if (isNaN(value)) { styleValue = 0; @@ -450,7 +516,6 @@ export class VectorStyle extends AbstractStyle { } _makeField(fieldDescriptor) { - if (!fieldDescriptor || !fieldDescriptor.name) { return null; } @@ -473,8 +538,6 @@ export class VectorStyle extends AbstractStyle { } else { throw new Error(`Unknown origin-type ${fieldDescriptor.origin}`); } - - } _makeSizeProperty(descriptor, styleName) { diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js index ea4228430d13d..b834fb842389e 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style_defaults.js @@ -16,8 +16,9 @@ const DEFAULT_ICON = 'airfield'; export const DEFAULT_MIN_SIZE = 1; export const DEFAULT_MAX_SIZE = 64; +export const DEFAULT_SIGMA = 3; -export const vectorStyles = { +export const VECTOR_STYLES = { SYMBOL: 'symbol', FILL_COLOR: 'fillColor', LINE_COLOR: 'lineColor', @@ -29,7 +30,7 @@ export const vectorStyles = { export function getDefaultProperties(mapColors = []) { return { ...getDefaultStaticProperties(mapColors), - [vectorStyles.SYMBOL]: { + [VECTOR_STYLES.SYMBOL]: { options: { symbolizeAs: SYMBOLIZE_AS_CIRCLE, symbolId: DEFAULT_ICON, @@ -48,31 +49,31 @@ export function getDefaultStaticProperties(mapColors = []) { return { - [vectorStyles.FILL_COLOR]: { + [VECTOR_STYLES.FILL_COLOR]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { color: nextFillColor, } }, - [vectorStyles.LINE_COLOR]: { + [VECTOR_STYLES.LINE_COLOR]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { color: nextLineColor } }, - [vectorStyles.LINE_WIDTH]: { + [VECTOR_STYLES.LINE_WIDTH]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { size: 1 } }, - [vectorStyles.ICON_SIZE]: { + [VECTOR_STYLES.ICON_SIZE]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { size: DEFAULT_ICON_SIZE } }, - [vectorStyles.ICON_ORIENTATION]: { + [VECTOR_STYLES.ICON_ORIENTATION]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { orientation: 0 @@ -83,40 +84,60 @@ export function getDefaultStaticProperties(mapColors = []) { export function getDefaultDynamicProperties() { return { - [vectorStyles.FILL_COLOR]: { + [VECTOR_STYLES.FILL_COLOR]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.LINE_COLOR]: { + [VECTOR_STYLES.LINE_COLOR]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { color: COLOR_GRADIENTS[0].value, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.LINE_WIDTH]: { + [VECTOR_STYLES.LINE_WIDTH]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { minSize: DEFAULT_MIN_SIZE, maxSize: DEFAULT_MAX_SIZE, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.ICON_SIZE]: { + [VECTOR_STYLES.ICON_SIZE]: { type: VectorStyle.STYLE_TYPE.DYNAMIC, options: { minSize: DEFAULT_MIN_SIZE, maxSize: DEFAULT_MAX_SIZE, field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, - [vectorStyles.ICON_ORIENTATION]: { + [VECTOR_STYLES.ICON_ORIENTATION]: { type: VectorStyle.STYLE_TYPE.STATIC, options: { field: undefined, + fieldMetaOptions: { + isEnabled: true, + sigma: DEFAULT_SIGMA, + } } }, }; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js index 610c704b34ec6..557a2bf869987 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.js @@ -128,3 +128,22 @@ export async function canSkipSourceUpdate({ source, prevDataRequest, nextMeta }) && !updateDueToPrecisionChange && !updateDueToSourceMetaChange; } + +export function canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }) { + if (!prevDataRequest) { + return false; + } + const prevMeta = prevDataRequest.getMeta(); + if (!prevMeta) { + return false; + } + + const updateDueToFields = !_.isEqual(prevMeta.dynamicStyleFields, nextMeta.dynamicStyleFields); + + const updateDueToSourceQuery = !_.isEqual(prevMeta.sourceQuery, nextMeta.sourceQuery); + + const updateDueToIsTimeAware = nextMeta.isTimeAware !== prevMeta.isTimeAware; + const updateDueToTime = nextMeta.isTimeAware ? !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters) : false; + + return !updateDueToFields && !updateDueToSourceQuery && !updateDueToIsTimeAware && !updateDueToTime; +} diff --git a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js index 77359a6def48f..24728f2ac95fd 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/can_skip_fetch.test.js @@ -126,7 +126,8 @@ describe('canSkipSourceUpdate', () => { applyGlobalQuery: prevApplyGlobalQuery, filters: prevFilters, query: prevQuery, - } + }, + data: {} }); it('can skip update when filter changes', async () => { @@ -210,7 +211,8 @@ describe('canSkipSourceUpdate', () => { applyGlobalQuery: prevApplyGlobalQuery, filters: prevFilters, query: prevQuery, - } + }, + data: {} }); it('can not skip update when filter changes', async () => { diff --git a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js b/x-pack/legacy/plugins/maps/public/layers/util/data_request.js index 95b82aa292884..12d57afbe1c87 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/data_request.js @@ -22,7 +22,7 @@ export class DataRequest { } getMeta() { - return _.get(this._descriptor, 'dataMeta', {}); + return this.hasData() ? _.get(this._descriptor, 'dataMeta', {}) : _.get(this._descriptor, 'dataMetaAtStart', {}); } hasData() { diff --git a/x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js b/x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js new file mode 100644 index 0000000000000..54d8794b1e3cf --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/layers/util/is_metric_countable.js @@ -0,0 +1,11 @@ +/* + * 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 { METRIC_TYPE } from '../../../common/constants'; + +export function isMetricCountable(aggType) { + return [METRIC_TYPE.COUNT, METRIC_TYPE.SUM, METRIC_TYPE.UNIQUE_COUNT].includes(aggType); +} diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 57126bb7681b8..7e831115e6dba 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -12,16 +12,19 @@ import { InnerJoin } from './joins/inner_join'; import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, + SOURCE_META_ID_ORIGIN, FEATURE_VISIBLE_PROPERTY_NAME, EMPTY_FEATURE_COLLECTION, - LAYER_TYPE + LAYER_TYPE, + FIELD_ORIGIN, + LAYER_STYLE_TYPE, } from '../../common/constants'; import _ from 'lodash'; import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { DataRequestAbortError } from './util/data_request'; -import { canSkipSourceUpdate } from './util/can_skip_fetch'; +import { canSkipSourceUpdate, canSkipStyleMetaUpdate } from './util/can_skip_fetch'; import { assignFeatureIds } from './util/assign_feature_ids'; import { getFillFilterExpression, @@ -88,7 +91,7 @@ export class VectorLayer extends AbstractLayer { const joins = this.getValidJoins(); for (let i = 0; i < joins.length; i++) { - const joinDataRequest = this.getDataRequest(joins[i].getSourceId()); + const joinDataRequest = this.getDataRequest(joins[i].getSourceDataRequestId()); if (!joinDataRequest || !joinDataRequest.hasData()) { return false; } @@ -229,12 +232,10 @@ export class VectorLayer extends AbstractLayer { return this._dataRequests.find(dataRequest => dataRequest.getDataId() === sourceDataId); } - - async _syncJoin({ join, startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const joinSource = join.getRightJoinSource(); - const sourceDataId = join.getSourceId(); + const sourceDataId = join.getSourceDataRequestId(); const requestToken = Symbol(`layer-join-refresh:${this.getId()} - ${sourceDataId}`); const searchFilters = { ...dataFilters, @@ -287,6 +288,7 @@ export class VectorLayer extends AbstractLayer { async _syncJoins(syncContext) { const joinSyncs = this.getValidJoins().map(async join => { + await this._syncJoinStyleMeta(syncContext, join); return this._syncJoin({ join, ...syncContext }); }); @@ -350,7 +352,7 @@ export class VectorLayer extends AbstractLayer { startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { - const requestToken = Symbol(`layer-source-refresh:${this.getId()} - source`); + const requestToken = Symbol(`layer-source-data:${this.getId()}`); const searchFilters = this._getSearchFilters(dataFilters); const prevDataRequest = this.getSourceDataRequest(); @@ -389,11 +391,89 @@ export class VectorLayer extends AbstractLayer { } } + async _syncSourceStyleMeta(syncContext) { + if (this._style.constructor.type !== LAYER_STYLE_TYPE.VECTOR) { + return; + } + + return this._syncStyleMeta({ + source: this._source, + sourceQuery: this.getQuery(), + dataRequestId: SOURCE_META_ID_ORIGIN, + dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.SOURCE && dynamicStyleProp.isFieldMetaEnabled(); + }), + ...syncContext + }); + } + + async _syncJoinStyleMeta(syncContext, join) { + const joinSource = join.getRightJoinSource(); + return this._syncStyleMeta({ + source: joinSource, + sourceQuery: joinSource.getWhereQuery(), + dataRequestId: join.getSourceMetaDataRequestId(), + dynamicStyleProps: this._style.getDynamicPropertiesArray().filter(dynamicStyleProp => { + const matchingField = joinSource.getMetricFieldForName(dynamicStyleProp.getField().getName()); + return dynamicStyleProp.getFieldOrigin() === FIELD_ORIGIN.JOIN + && !!matchingField + && dynamicStyleProp.isFieldMetaEnabled(); + }), + ...syncContext + }); + } + + async _syncStyleMeta({ + source, + sourceQuery, + dataRequestId, + dynamicStyleProps, + dataFilters, + startLoading, + stopLoading, + onLoadError, + registerCancelCallback + }) { + + if (!source.isESSource() || dynamicStyleProps.length === 0) { + return; + } + + const dynamicStyleFields = dynamicStyleProps.map(dynamicStyleProp => { + return dynamicStyleProp.getField().getName(); + }); + + const nextMeta = { + dynamicStyleFields: _.uniq(dynamicStyleFields).sort(), + sourceQuery, + isTimeAware: this._style.isTimeAware() && await source.isTimeAware(), + timeFilters: dataFilters.timeFilters, + }; + const prevDataRequest = this._findDataRequestForSource(dataRequestId); + const canSkipFetch = canSkipStyleMetaUpdate({ prevDataRequest, nextMeta }); + if (canSkipFetch) { + return; + } + + const requestToken = Symbol(`layer-${this.getId()}-style-meta`); + try { + startLoading(dataRequestId, requestToken, nextMeta); + const layerName = await this.getDisplayName(); + const styleMeta = await source.loadStylePropsMeta(layerName, this._style, dynamicStyleProps, registerCancelCallback, nextMeta); + stopLoading(dataRequestId, requestToken, styleMeta, nextMeta); + } catch (error) { + if (!(error instanceof DataRequestAbortError)) { + onLoadError(dataRequestId, requestToken, error.message); + } + } + } + async syncData(syncContext) { if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; } + await this._syncSourceStyleMeta(syncContext); const sourceResult = await this._syncSource(syncContext); if ( !sourceResult.featureCollection || diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts index d7da585966758..ede0d3f394789 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts @@ -147,6 +147,10 @@ export const mockLineLayer = { }, minSize: 1, maxSize: 8, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, }, }, iconSize: { type: 'STATIC', options: { size: 10 } }, diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts index fd17e6eaeac64..637251eb64f70 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_config.ts @@ -210,6 +210,10 @@ export const getLineLayer = (indexPatternTitle: string, indexPatternId: string) }, minSize: 1, maxSize: 8, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, }, }, iconSize: { type: 'STATIC', options: { size: 10 } }, diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 1291e3dd10cff..a9d2601442aaa 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -411,7 +411,7 @@ "type": "envelope" }, "description": "", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"label\":\"EMS base layer (road_map)\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[\"name\"],\"applyGlobalQuery\":false,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name\",\"origin\":\"join\"},\"color\":\"Blues\"}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_join_0_index_pattern\"}}]}]", + "layerListJSON" : "[{\"id\":\"0hmz5\",\"label\":\"EMS base layer (road_map)\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"n1t6f\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"62eca1fc-fe42-11e8-8eb2-f2801f1b9fd1\",\"type\":\"ES_SEARCH\",\"geoField\":\"geometry\",\"limit\":2048,\"filterByMapBounds\":false,\"showTooltip\":true,\"tooltipProperties\":[\"name\"],\"applyGlobalQuery\":false,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"fieldMetaOptions\":{\"isEnabled\":false,\"sigma\":3},\"field\":{\"label\":\"max(prop1) group by meta_for_geo_shapes*.shape_name\",\"name\":\"__kbnjoin__max_of_prop1_groupby_meta_for_geo_shapes*.shape_name\",\"origin\":\"join\"},\"color\":\"Blues\"}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"VECTOR\",\"joins\":[{\"leftField\":\"name\",\"right\":{\"id\":\"855ccb86-fe42-11e8-8eb2-f2801f1b9fd1\",\"indexPatternTitle\":\"meta_for_geo_shapes*\",\"term\":\"shape_name\",\"metrics\":[{\"type\":\"max\",\"field\":\"prop1\"}],\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_join_0_index_pattern\"}}]}]", "mapStateJSON": "{\"zoom\":3.02,\"center\":{\"lon\":77.33426,\"lat\":-0.04647},\"timeFilters\":{\"from\":\"now-17m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", "title": "join example", "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[\"n1t6f\"]}" From 2ef6d8d8f7b4ed4b1a2a9a06c5305af1f9c88ae7 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Fri, 6 Dec 2019 17:46:25 +0100 Subject: [PATCH 02/26] Add pre-response http interceptor (#52366) * add onPreResponse interceptor * expose registerPreResponse to plugins * address comments * regen docs --- .../kibana-plugin-server.httpservicesetup.md | 1 + ....httpservicesetup.registeronpreresponse.md | 18 ++ .../core/server/kibana-plugin-server.md | 4 + ...-server.onpreresponseextensions.headers.md | 13 ++ ...a-plugin-server.onpreresponseextensions.md | 20 +++ ...bana-plugin-server.onpreresponsehandler.md | 13 ++ .../kibana-plugin-server.onpreresponseinfo.md | 20 +++ ...gin-server.onpreresponseinfo.statuscode.md | 11 ++ ...bana-plugin-server.onpreresponsetoolkit.md | 20 +++ ...plugin-server.onpreresponsetoolkit.next.md | 13 ++ src/core/server/http/http_server.ts | 50 ++---- src/core/server/http/http_service.mock.ts | 1 + src/core/server/http/index.ts | 6 + .../http/integration_tests/lifecycle.test.ts | 168 +++++++++++++++++- .../server/http/lifecycle/on_pre_response.ts | 155 ++++++++++++++++ src/core/server/http/types.ts | 13 ++ src/core/server/index.ts | 4 + src/core/server/legacy/legacy_service.ts | 1 + src/core/server/mocks.ts | 1 + src/core/server/plugins/plugin_context.ts | 1 + src/core/server/server.api.md | 22 +++ 21 files changed, 517 insertions(+), 38 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md create mode 100644 docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md create mode 100644 src/core/server/http/lifecycle/on_pre_response.ts diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index dba0ad8c8560c..25eebf1c06d01 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -23,6 +23,7 @@ export interface HttpServiceSetup | [registerAuth](./kibana-plugin-server.httpservicesetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. | | [registerOnPostAuth](./kibana-plugin-server.httpservicesetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. | | [registerOnPreAuth](./kibana-plugin-server.httpservicesetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests. | +| [registerOnPreResponse](./kibana-plugin-server.httpservicesetup.registeronpreresponse.md) | (handler: OnPreResponseHandler) => void | To define custom logic to perform for the server response. | | [registerRouteHandlerContext](./kibana-plugin-server.httpservicesetup.registerroutehandlercontext.md) | <T extends keyof RequestHandlerContext>(contextName: T, provider: RequestHandlerContextProvider<T>) => RequestHandlerContextContainer | Register a context provider for a route handler. | ## Example diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md new file mode 100644 index 0000000000000..9f0eaae8830e1 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.registeronpreresponse.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) > [registerOnPreResponse](./kibana-plugin-server.httpservicesetup.registeronpreresponse.md) + +## HttpServiceSetup.registerOnPreResponse property + +To define custom logic to perform for the server response. + +Signature: + +```typescript +registerOnPreResponse: (handler: OnPreResponseHandler) => void; +``` + +## Remarks + +Doesn't provide the whole response object. Supports extending response with custom headers. See [OnPreResponseHandler](./kibana-plugin-server.onpreresponsehandler.md). + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 9144742c9bb73..fceabd1237665 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -77,6 +77,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | | [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | +| [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) | Additional data to extend a response. | +| [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) | Response status code. | +| [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration schema and capabilities. | @@ -173,6 +176,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [MutatingOperationRefreshSetting](./kibana-plugin-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | | [OnPostAuthHandler](./kibana-plugin-server.onpostauthhandler.md) | See [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md). | | [OnPreAuthHandler](./kibana-plugin-server.onpreauthhandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | +| [OnPreResponseHandler](./kibana-plugin-server.onpreresponsehandler.md) | See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). | | [PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | Dedicated type for plugin configuration schema. | | [PluginInitializer](./kibana-plugin-server.plugininitializer.md) | The plugin export at the root of a plugin's server directory should conform to this interface. | | [PluginName](./kibana-plugin-server.pluginname.md) | Dedicated type for plugin name/id that is supposed to make Map/Set/Arrays that use it as a key or value more obvious. | diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md new file mode 100644 index 0000000000000..8736020daf063 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.headers.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) > [headers](./kibana-plugin-server.onpreresponseextensions.headers.md) + +## OnPreResponseExtensions.headers property + +additional headers to attach to the response + +Signature: + +```typescript +headers?: ResponseHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md new file mode 100644 index 0000000000000..e5aa624c39909 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseextensions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) + +## OnPreResponseExtensions interface + +Additional data to extend a response. + +Signature: + +```typescript +export interface OnPreResponseExtensions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [headers](./kibana-plugin-server.onpreresponseextensions.headers.md) | ResponseHeaders | additional headers to attach to the response | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md b/docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md new file mode 100644 index 0000000000000..082de0a9b4aeb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponsehandler.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseHandler](./kibana-plugin-server.onpreresponsehandler.md) + +## OnPreResponseHandler type + +See [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md). + +Signature: + +```typescript +export declare type OnPreResponseHandler = (request: KibanaRequest, preResponse: OnPreResponseInfo, toolkit: OnPreResponseToolkit) => OnPreResponseResult | Promise; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md new file mode 100644 index 0000000000000..736b4298037cf --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) + +## OnPreResponseInfo interface + +Response status code. + +Signature: + +```typescript +export interface OnPreResponseInfo +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [statusCode](./kibana-plugin-server.onpreresponseinfo.statuscode.md) | number | | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md new file mode 100644 index 0000000000000..4fd4529dc400f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponseinfo.statuscode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) > [statusCode](./kibana-plugin-server.onpreresponseinfo.statuscode.md) + +## OnPreResponseInfo.statusCode property + +Signature: + +```typescript +statusCode: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md new file mode 100644 index 0000000000000..5525f5bf60284 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) + +## OnPreResponseToolkit interface + +A tool set defining an outcome of OnPreAuth interceptor for incoming request. + +Signature: + +```typescript +export interface OnPreResponseToolkit +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [next](./kibana-plugin-server.onpreresponsetoolkit.next.md) | (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult | To pass request to the next handler | + diff --git a/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md new file mode 100644 index 0000000000000..bfb5827b16b2f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.onpreresponsetoolkit.next.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) > [next](./kibana-plugin-server.onpreresponsetoolkit.next.md) + +## OnPreResponseToolkit.next property + +To pass request to the next handler + +Signature: + +```typescript +next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; +``` diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index f77184fb79ab6..244b3cca60f31 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - -import { Request, Server } from 'hapi'; +import { Server } from 'hapi'; import url from 'url'; import { Logger, LoggerFactory } from '../logging'; @@ -26,8 +25,9 @@ import { createServer, getListenerOptions, getServerOptions } from './http_tools import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; +import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response'; -import { ResponseHeaders, IRouter } from './router'; +import { IRouter } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -50,6 +50,7 @@ export interface HttpServerSetup { registerAuth: HttpServiceSetup['registerAuth']; registerOnPreAuth: HttpServiceSetup['registerOnPreAuth']; registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; + registerOnPreResponse: HttpServiceSetup['registerOnPreResponse']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; auth: { get: GetAuthState; @@ -103,6 +104,7 @@ export class HttpServer { registerRouter: this.registerRouter.bind(this), registerOnPreAuth: this.registerOnPreAuth.bind(this), registerOnPostAuth: this.registerOnPostAuth.bind(this), + registerOnPreResponse: this.registerOnPreResponse.bind(this), createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) => this.createCookieSessionStorageFactory(cookieOptions, config.basePath), registerAuth: this.registerAuth.bind(this), @@ -232,6 +234,14 @@ export class HttpServer { this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log)); } + private registerOnPreResponse(fn: OnPreResponseHandler) { + if (this.server === undefined) { + throw new Error('Server is not created yet'); + } + + this.server.ext('onPreResponse', adoptToHapiOnPreResponseFormat(fn, this.log)); + } + private async createCookieSessionStorageFactory( cookieOptions: SessionStorageCookieOptions, basePath?: string @@ -289,39 +299,9 @@ export class HttpServer { // https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions this.server.auth.default('session'); - this.server.ext('onPreResponse', (request, t) => { + this.registerOnPreResponse((request, preResponseInfo, t) => { const authResponseHeaders = this.authResponseHeaders.get(request); - this.extendResponseWithHeaders(request, authResponseHeaders); - return t.continue; - }); - } - - private extendResponseWithHeaders(request: Request, headers?: ResponseHeaders) { - const response = request.response; - if (!headers || !response) return; - - if (response instanceof Error) { - this.findHeadersIntersection(response.output.headers, headers); - // hapi wraps all error response in Boom object internally - response.output.headers = { - ...response.output.headers, - ...(headers as any), // hapi types don't specify string[] as valid value - }; - } else { - for (const [headerName, headerValue] of Object.entries(headers)) { - this.findHeadersIntersection(response.headers, headers); - response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value - } - } - } - - // NOTE: responseHeaders contains not a full list of response headers, but only explicitly set on a response object. - // any headers added by hapi internally, like `content-type`, `content-length`, etc. do not present here. - private findHeadersIntersection(responseHeaders: ResponseHeaders, headers: ResponseHeaders) { - Object.keys(headers).forEach(headerName => { - if (responseHeaders[headerName] !== undefined) { - this.log.warn(`Server rewrites a response header [${headerName}].`); - } + return t.next({ headers: authResponseHeaders }); }); } } diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index c7f6cdb2bb422..fb3716c42b831 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -51,6 +51,7 @@ const createSetupContractMock = () => { registerAuth: jest.fn(), registerOnPostAuth: jest.fn(), registerRouteHandlerContext: jest.fn(), + registerOnPreResponse: jest.fn(), createRouter: jest.fn().mockImplementation(() => mockRouter.create({})), basePath: createBasePathMock(), auth: { diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index f9a3a91ec18ad..21de3945f1044 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -64,6 +64,12 @@ export { AuthResultType, } from './lifecycle/auth'; export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth'; +export { + OnPreResponseHandler, + OnPreResponseToolkit, + OnPreResponseExtensions, + OnPreResponseInfo, +} from './lifecycle/on_pre_response'; export { SessionStorageFactory, SessionStorage } from './session_storage'; export { SessionStorageCookieOptions, diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 2a32db77377a4..0edbcf19d3209 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -161,7 +161,7 @@ describe('OnPreAuth', () => { expect(result.header['www-authenticate']).toBe('challenge'); }); - it("doesn't expose error details if interceptor throws", async () => { + it('does not expose error details if interceptor throws', async () => { const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps); const router = createRouter('/'); @@ -734,7 +734,7 @@ describe('Auth', () => { expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` Array [ Array [ - "Server rewrites a response header [www-authenticate].", + "onPreResponseHandler rewrote a response header [www-authenticate].", ], ] `); @@ -769,7 +769,7 @@ describe('Auth', () => { expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` Array [ Array [ - "Server rewrites a response header [www-authenticate].", + "onPreResponseHandler rewrote a response header [www-authenticate].", ], ] `); @@ -893,3 +893,165 @@ describe('Auth', () => { .expect(200, { customField: 'undefined' }); }); }); + +describe('OnPreResponse', () => { + it('supports registering response inceptors', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' })); + + const callingOrder: string[] = []; + registerOnPreResponse((req, res, t) => { + callingOrder.push('first'); + return t.next(); + }); + + registerOnPreResponse((req, res, t) => { + callingOrder.push('second'); + return t.next(); + }); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200, 'ok'); + + expect(callingOrder).toEqual(['first', 'second']); + }); + + it('supports additional headers attachments', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => + res.ok({ + headers: { + 'x-my-header': 'foo', + }, + }) + ); + + registerOnPreResponse((req, res, t) => + t.next({ + headers: { + 'x-kibana-header': 'value', + }, + }) + ); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(result.header['x-kibana-header']).toBe('value'); + expect(result.header['x-my-header']).toBe('foo'); + }); + + it('logs a warning if interceptor rewrites response header', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => + res.ok({ + headers: { 'x-kibana-header': 'value' }, + }) + ); + + registerOnPreResponse((req, res, t) => + t.next({ + headers: { 'x-kibana-header': 'value' }, + }) + ); + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200); + + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "onPreResponseHandler rewrote a response header [x-kibana-header].", + ], + ] + `); + }); + + it("doesn't expose error details if interceptor throws", async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(undefined)); + registerOnPreResponse((req, res, t) => { + throw new Error('reason'); + }); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: reason], + ], + ] + `); + }); + + it('returns internal error if interceptor returns unexpected result', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok()); + registerOnPreResponse((req, res, t) => ({} as any)); + await server.start(); + + const result = await supertest(innerServer.listener) + .get('/') + .expect(500); + + expect(result.body.message).toBe('An internal server error occurred.'); + expect(loggingServiceMock.collect(logger).error).toMatchInlineSnapshot(` + Array [ + Array [ + [Error: Unexpected result from OnPreResponse. Expected OnPreResponseResult, but given: [object Object].], + ], + ] + `); + }); + + it('cannot change response statusCode', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + registerOnPreResponse((req, res, t) => { + res.statusCode = 500; + return t.next(); + }); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' })); + + await server.start(); + + await supertest(innerServer.listener) + .get('/') + .expect(200); + }); +}); diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts new file mode 100644 index 0000000000000..45d7478df9805 --- /dev/null +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -0,0 +1,155 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi'; +import Boom from 'boom'; +import { Logger } from '../../logging'; + +import { HapiResponseAdapter, KibanaRequest, ResponseHeaders } from '../router'; + +enum ResultType { + next = 'next', +} + +interface Next { + type: ResultType.next; + headers?: ResponseHeaders; +} + +/** + * @internal + */ +type OnPreResponseResult = Next; + +/** + * Additional data to extend a response. + * @public + */ +export interface OnPreResponseExtensions { + /** additional headers to attach to the response */ + headers?: ResponseHeaders; +} + +/** + * Response status code. + * @public + */ +export interface OnPreResponseInfo { + statusCode: number; +} + +const preResponseResult = { + next(responseExtensions?: OnPreResponseExtensions): OnPreResponseResult { + return { type: ResultType.next, headers: responseExtensions?.headers }; + }, + isNext(result: OnPreResponseResult): result is Next { + return result && result.type === ResultType.next; + }, +}; + +/** + * A tool set defining an outcome of OnPreAuth interceptor for incoming request. + * @public + */ +export interface OnPreResponseToolkit { + /** To pass request to the next handler */ + next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; +} + +const toolkit: OnPreResponseToolkit = { + next: preResponseResult.next, +}; + +/** + * See {@link OnPreAuthToolkit}. + * @public + */ +export type OnPreResponseHandler = ( + request: KibanaRequest, + preResponse: OnPreResponseInfo, + toolkit: OnPreResponseToolkit +) => OnPreResponseResult | Promise; + +/** + * @public + * Adopt custom request interceptor to Hapi lifecycle system. + * @param fn - an extension point allowing to perform custom logic for + * incoming HTTP requests. + */ +export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Logger) { + return async function interceptPreResponse( + request: Request, + responseToolkit: HapiResponseToolkit + ): Promise { + const response = request.response; + + try { + if (response) { + const statusCode: number = isBoom(response) + ? response.output.statusCode + : response.statusCode; + + const result = await fn(KibanaRequest.from(request), { statusCode }, toolkit); + if (!preResponseResult.isNext(result)) { + throw new Error( + `Unexpected result from OnPreResponse. Expected OnPreResponseResult, but given: ${result}.` + ); + } + if (result.headers) { + if (isBoom(response)) { + findHeadersIntersection(response.output.headers, result.headers, log); + // hapi wraps all error response in Boom object internally + response.output.headers = { + ...response.output.headers, + ...(result.headers as any), // hapi types don't specify string[] as valid value + }; + } else { + for (const [headerName, headerValue] of Object.entries(result.headers)) { + findHeadersIntersection(response.headers, result.headers, log); + response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value + } + } + } + } + } catch (error) { + log.error(error); + const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); + return hapiResponseAdapter.toInternalError(); + } + return responseToolkit.continue; + }; +} + +function isBoom(response: any): response is Boom { + return response instanceof Boom; +} + +// NOTE: responseHeaders contains not a full list of response headers, but only explicitly set on a response object. +// any headers added by hapi internally, like `content-type`, `content-length`, etc. are not present here. +function findHeadersIntersection( + responseHeaders: ResponseHeaders, + headers: ResponseHeaders, + log: Logger +) { + Object.keys(headers).forEach(headerName => { + if (responseHeaders[headerName] !== undefined) { + log.warn(`onPreResponseHandler rewrote a response header [${headerName}].`); + } + }); +} diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts index 2c3dfedd1d181..94c1982a18c0a 100644 --- a/src/core/server/http/types.ts +++ b/src/core/server/http/types.ts @@ -24,6 +24,7 @@ import { SessionStorageFactory } from './session_storage'; import { AuthenticationHandler } from './lifecycle/auth'; import { OnPreAuthHandler } from './lifecycle/on_pre_auth'; import { OnPostAuthHandler } from './lifecycle/on_post_auth'; +import { OnPreResponseHandler } from './lifecycle/on_pre_response'; import { IBasePath } from './base_path_service'; import { PluginOpaqueId, RequestHandlerContext } from '..'; @@ -163,6 +164,18 @@ export interface HttpServiceSetup { */ registerOnPostAuth: (handler: OnPostAuthHandler) => void; + /** + * To define custom logic to perform for the server response. + * + * @remarks + * Doesn't provide the whole response object. + * Supports extending response with custom headers. + * See {@link OnPreResponseHandler}. + * + * @param handler {@link OnPreResponseHandler} - function to call. + */ + registerOnPreResponse: (handler: OnPreResponseHandler) => void; + /** * Access or manipulate the Kibana base path * See {@link IBasePath}. diff --git a/src/core/server/index.ts b/src/core/server/index.ts index efff85142c3e4..57156322e2849 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -105,6 +105,10 @@ export { OnPreAuthToolkit, OnPostAuthHandler, OnPostAuthToolkit, + OnPreResponseHandler, + OnPreResponseToolkit, + OnPreResponseExtensions, + OnPreResponseInfo, RedirectResponseOptions, RequestHandler, RequestHandlerContextContainer, diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 5d111884144c1..fcf0c45c17db8 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -270,6 +270,7 @@ export class LegacyService implements CoreService { registerOnPreAuth: setupDeps.core.http.registerOnPreAuth, registerAuth: setupDeps.core.http.registerAuth, registerOnPostAuth: setupDeps.core.http.registerOnPostAuth, + registerOnPreResponse: setupDeps.core.http.registerOnPreResponse, basePath: setupDeps.core.http.basePath, isTlsEnabled: setupDeps.core.http.isTlsEnabled, }, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 8f864dda6b9f3..c07caaa04ba52 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -90,6 +90,7 @@ function createCoreSetupMock() { registerOnPreAuth: httpService.registerOnPreAuth, registerAuth: httpService.registerAuth, registerOnPostAuth: httpService.registerOnPostAuth, + registerOnPreResponse: httpService.registerOnPreResponse, basePath: httpService.basePath, isTlsEnabled: httpService.isTlsEnabled, createRouter: jest.fn(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index dfd1052bbec75..6829784e6e0a1 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -159,6 +159,7 @@ export function createPluginSetupContext( registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, registerOnPostAuth: deps.http.registerOnPostAuth, + registerOnPreResponse: deps.http.registerOnPreResponse, basePath: deps.http.basePath, isTlsEnabled: deps.http.isTlsEnabled, }, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7e1226aa7238b..c855e04e420f7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -697,6 +697,7 @@ export interface HttpServiceSetup { registerAuth: (handler: AuthenticationHandler) => void; registerOnPostAuth: (handler: OnPostAuthHandler) => void; registerOnPreAuth: (handler: OnPreAuthHandler) => void; + registerOnPreResponse: (handler: OnPreResponseHandler) => void; registerRouteHandlerContext: (contextName: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer; } @@ -976,6 +977,27 @@ export interface OnPreAuthToolkit { rewriteUrl: (url: string) => OnPreAuthResult; } +// @public +export interface OnPreResponseExtensions { + headers?: ResponseHeaders; +} + +// Warning: (ae-forgotten-export) The symbol "OnPreResponseResult" needs to be exported by the entry point index.d.ts +// +// @public +export type OnPreResponseHandler = (request: KibanaRequest, preResponse: OnPreResponseInfo, toolkit: OnPreResponseToolkit) => OnPreResponseResult | Promise; + +// @public +export interface OnPreResponseInfo { + // (undocumented) + statusCode: number; +} + +// @public +export interface OnPreResponseToolkit { + next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; +} + // @public (undocumented) export interface PackageInfo { // (undocumented) From 20d30e5b27f2879c510a4832f79c1e45b02fa616 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 6 Dec 2019 17:42:45 +0000 Subject: [PATCH 03/26] chore(NA): add resolution to bump serialize-javascript (#52336) --- package.json | 3 ++- yarn.lock | 13 ++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index c3d19367e8b92..2b157da779f63 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ "**/image-diff/gm/debug": "^2.6.9", "**/react-dom": "^16.12.0", "**/react-test-renderer": "^16.12.0", - "**/deepmerge": "^4.2.2" + "**/deepmerge": "^4.2.2", + "**/serialize-javascript": "^2.1.1" }, "workspaces": { "packages": [ diff --git a/yarn.lock b/yarn.lock index 49216f9dac056..b4960a6cd01e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25355,15 +25355,10 @@ sentence-case@^2.1.0: no-case "^2.2.0" upper-case-first "^1.1.2" -serialize-javascript@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" - integrity sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA== - -serialize-javascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.0.tgz#9310276819efd0eb128258bb341957f6eb2fc570" - integrity sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ== +serialize-javascript@^1.7.0, serialize-javascript@^2.1.0, serialize-javascript@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.1.tgz#952907a04a3e3a75af7f73d92d15e233862048b2" + integrity sha512-MPLPRpD4FNqWq9tTIjYG5LesFouDhdyH0EPY3gVK4DRD5+g4aDqdNSzLIwceulo3Yj+PL1bPh6laE5+H6LTcrQ== serve-favicon@^2.5.0: version "2.5.0" From 3368ce096c5d3a57344767fbc728fea21dbff3dd Mon Sep 17 00:00:00 2001 From: Matt Bargar Date: Fri, 6 Dec 2019 13:04:26 -0500 Subject: [PATCH 04/26] Preserve currently loaded Saved Query in Discover when page reloads (#52323) * Fix import * Add test that would have failed with previous bug --- .../kibana/public/discover/angular/discover.js | 4 ++-- test/functional/apps/discover/_saved_queries.js | 12 ++++++++++++ .../services/saved_query_management_component.ts | 9 +++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js index 7abb7166aa902..ec0c5c34f7a93 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/discover.js @@ -79,7 +79,7 @@ import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { registerTimefilterWithGlobalStateFactory } from '../../../../../ui/public/timefilter/setup_router'; import { FilterStateManager } from '../../../../data/public/filter/filter_manager'; -const { savedQueryService } = data.query.savedQueries; +const { getSavedQuery } = data.query.savedQueries; const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -972,7 +972,7 @@ function discoverController( return; } if (!$scope.savedQuery || newSavedQueryId !== $scope.savedQuery.id) { - savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery) => { + getSavedQuery(newSavedQueryId).then((savedQuery) => { $scope.$evalAsync(() => { $scope.savedQuery = savedQuery; updateStateFromSavedQuery(savedQuery); diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 8fbc40f86e8dc..3ae8f51fb76dc 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + const browser = getService('browser'); const defaultSettings = { defaultIndex: 'logstash-*', @@ -86,6 +87,17 @@ export default function ({ getService, getPageObjects }) { expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); }); + it('preserves the currently loaded query when the page is reloaded', async () => { + await browser.refresh(); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + expect(await PageObjects.discover.getHitCount()).to.be('2,792'); + expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse'); + }); + + it('allows saving changes to a currently loaded query via the saved query management component', async () => { await queryBar.setQuery('response:404'); await savedQueryManagementComponent.updateCurrentlyLoadedQuery( diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index d6de0be0c172e..9f0a8ded649b2 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -26,6 +26,15 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide const retry = getService('retry'); class SavedQueryManagementComponent { + public async getCurrentlyLoadedQueryID() { + await this.openSavedQueryManagementComponent(); + try { + return await testSubjects.getVisibleText('~saved-query-list-item-selected'); + } catch { + return undefined; + } + } + public async saveNewQuery( name: string, description: string, From ab5913d1092633035a3261669642f79f68881d47 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Fri, 6 Dec 2019 13:35:34 -0500 Subject: [PATCH 05/26] Infra server NP shim + config/routing API adoption (#45299) * Basic cleanup before refactoring for shim work * shim WIP * Removes the configuration adapter * WIP more stuff * WIP refactoring of shimming work * WIP continues * Logging UI now runs on top of new platform shim * WIP continues * Removes unused imports and variables * Basic infra NP server shim in place * Reimplemented graphql http error handling for infra NP server shim * Adds new platform infra plugin to handle NP config for legacy server shim * Basic cleanup before refactoring for shim work * shim WIP * Removes the configuration adapter * WIP more stuff * WIP refactoring of shimming work * WIP continues * Logging UI now runs on top of new platform shim * WIP continues * Removes unused imports and variables * Basic infra NP server shim in place * Reimplemented graphql http error handling for infra NP server shim * Adds new platform infra plugin to handle NP config for legacy server shim * Adds comment about duplicating full config for NP config * Use New Platform features plugin to registerFeature() * Re-arranging and relying on request context as uch as possible * Refactors KibanaRequest for RequestHandlerContext * fixes types for callWithRequest * Moves callWithRequest method override types directly into class to get them working, need to fix this when we understand it better * Fixes callWithRequest framework types * Removes a few NP_TODO comments * Fix broken imports * Ensure GraphQL resolvers are actually passed requestContext and not the raw request, and switch to the savedObjects client via requestContext * Remove the legacy traces of the savedObjects plugin * Fixes TSVB access with NP raw requests and requestContext * Remove unused getUiSettingsService (moved to requestContext) * Migrate to new Spaces plugin * Fix calculateMetricInterval after merged changes * Reinstate and migrate the infrastructure metadata route * Fix various type check errors * Amend InfraSources lib unit tests Mock the savedObjects client differently * Amend MetricsExplorer API response Renaming of variable inadvertently broke the response * Remove GraphQLI references from feature controls tests * Remove other GraphiQL references * Fix security / access issue * Add a framework level registerRoute method which always adds access tags by default * *Temp* disable test * Migrate the log rate validation endpoint to the new platform Fully migrates the [Logs UI] log rate setup index validation #50008 PR to New Platform routing etc * Amend types * Example of how to expose APM get indices method in NP * Fix calls to TSVB bug caused by object mutation This is a temp fix as the TSVB NP migration will supercede this * Converts getApmIndices function to accept saved object client, implements usage in infra * Fix APM setup_request tests * Fixes some unused references for linting * Migrate all work from #50730 to NP * Remove duplicate declaration files for rison_node and add a single source of truth at x-pack/typings/rison_node.d.ts for x-pack uses * Moved type file back into infra plugin to bypass strange break * Updates apm indices method signature per feedback from @elastic/apm-ui --- .../apm/server/lib/helpers/es_client.ts | 5 +- .../server/lib/helpers/setup_request.test.ts | 10 + .../apm/server/lib/helpers/setup_request.ts | 5 +- .../settings/apm_indices/get_apm_indices.ts | 18 +- .../apm/server/routes/settings/apm_indices.ts | 5 +- .../infra/common/http_api/metadata_api.ts | 4 +- .../infra/common/http_api/node_details_api.ts | 3 - .../infra/common/http_api/snapshot_api.ts | 2 - x-pack/legacy/plugins/infra/index.ts | 58 ++- .../framework/kibana_framework_adapter.ts | 2 +- .../public/lib/compose/kibana_compose.ts | 4 +- .../public/lib/compose/testing_compose.ts | 4 +- .../legacy/plugins/infra/server/features.ts | 65 +++ .../plugins/infra/server/infra_server.ts | 2 +- .../plugins/infra/server/kibana.index.ts | 92 +---- .../adapters/configuration/adapter_types.ts | 19 - .../lib/adapters/configuration/index.ts | 7 - .../inmemory_configuration_adapter.ts | 16 - .../kibana_configuration_adapter.test.ts | 40 -- .../kibana_configuration_adapter.ts | 73 ---- .../lib/adapters/fields/adapter_types.ts | 4 +- .../fields/framework_fields_adapter.ts | 26 +- .../lib/adapters/framework/adapter_types.ts | 141 ++----- .../adapters/framework/apollo_server_hapi.ts | 117 ------ .../framework/kibana_framework_adapter.ts | 377 +++++++++++------- .../log_entries/kibana_log_entries_adapter.ts | 27 +- .../lib/adapters/metrics/adapter_types.ts | 8 +- .../metrics/kibana_metrics_adapter.ts | 33 +- .../elasticsearch_source_status_adapter.ts | 24 +- .../infra/server/lib/compose/kibana.ts | 20 +- .../infra/server/lib/domains/fields_domain.ts | 11 +- .../log_entries_domain/log_entries_domain.ts | 65 +-- .../server/lib/domains/metrics_domain.ts | 9 +- .../plugins/infra/server/lib/infra_types.ts | 19 +- .../server/lib/log_analysis/log_analysis.ts | 15 +- .../infra/server/lib/snapshot/snapshot.ts | 30 +- .../plugins/infra/server/lib/source_status.ts | 71 +++- .../infra/server/lib/sources/sources.test.ts | 122 +++--- .../infra/server/lib/sources/sources.ts | 99 ++--- .../infra/server/new_platform_index.ts | 16 + .../infra/server/new_platform_plugin.ts | 107 +++++ .../infra/server/routes/ip_to_hostname.ts | 58 ++- .../log_analysis/index_patterns/validate.ts | 101 ++--- .../log_analysis/results/log_entry_rate.ts | 67 ++-- .../infra/server/routes/metadata/index.ts | 51 ++- .../metadata/lib/get_cloud_metric_metadata.ts | 10 +- .../metadata/lib/get_metric_metadata.ts | 10 +- .../routes/metadata/lib/get_node_info.ts | 16 +- .../routes/metadata/lib/get_pod_node_name.ts | 12 +- .../routes/metadata/lib/has_apm_data.ts | 20 +- .../server/routes/metrics_explorer/index.ts | 42 +- .../lib/create_metrics_model.ts | 4 +- .../metrics_explorer/lib/get_groupings.ts | 4 +- .../lib/populate_series_with_tsvb_data.ts | 26 +- .../server/routes/metrics_explorer/types.ts | 6 +- .../infra/server/routes/node_details/index.ts | 50 ++- .../infra/server/routes/snapshot/index.ts | 68 ++-- .../server/utils/calculate_metric_interval.ts | 13 +- .../server/utils/get_all_composite_data.ts | 20 +- x-pack/plugins/apm/server/plugin.ts | 17 +- x-pack/plugins/infra/kibana.json | 5 + x-pack/plugins/infra/server/index.ts | 24 ++ x-pack/plugins/infra/server/plugin.ts | 33 ++ .../apis/infra/feature_controls.ts | 41 -- x-pack/test/typings/rison_node.d.ts | 26 -- 65 files changed, 1263 insertions(+), 1236 deletions(-) create mode 100644 x-pack/legacy/plugins/infra/server/features.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts delete mode 100644 x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts create mode 100644 x-pack/legacy/plugins/infra/server/new_platform_index.ts create mode 100644 x-pack/legacy/plugins/infra/server/new_platform_plugin.ts create mode 100644 x-pack/plugins/infra/kibana.json create mode 100644 x-pack/plugins/infra/server/index.ts create mode 100644 x-pack/plugins/infra/server/plugin.ts delete mode 100644 x-pack/test/typings/rison_node.d.ts diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts index 28035ac2f9be2..c2dce4f4638ae 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/es_client.ts @@ -78,7 +78,10 @@ async function getParamsForSearchRequest( ) { const { uiSettings } = context.core; const [indices, includeFrozen] = await Promise.all([ - getApmIndices(context), + getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config: context.config + }), uiSettings.client.get('search:includeFrozen') ]); diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts index f320712d6151f..4272bdbddd26b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -50,6 +50,11 @@ function getMockRequest() { client: { get: jest.fn().mockResolvedValue(false) } + }, + savedObjects: { + client: { + get: jest.fn() + } } } } as unknown) as APMRequestHandlerContext & { @@ -65,6 +70,11 @@ function getMockRequest() { get: jest.Mock; }; }; + savedObjects: { + client: { + get: jest.Mock; + }; + }; }; }; diff --git a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts index a09cdbf91ec6e..56c9255844009 100644 --- a/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/legacy/plugins/apm/server/lib/helpers/setup_request.ts @@ -73,7 +73,10 @@ export async function setupRequest( const { config } = context; const { query } = context.params; - const indices = await getApmIndices(context); + const indices = await getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config + }); const dynamicIndexPattern = await getDynamicIndexPattern({ context, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index 0ed30ec4cdd27..e451f89af5620 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -54,15 +54,25 @@ export function getApmIndicesConfig(config: APMConfig): ApmIndicesConfig { }; } -export async function getApmIndices(context: APMRequestHandlerContext) { +// export async function getApmIndices(context: APMRequestHandlerContext) { +// return _getApmIndices(context.core, context.config); +// } + +export async function getApmIndices({ + config, + savedObjectsClient +}: { + config: APMConfig; + savedObjectsClient: SavedObjectsClientContract; +}) { try { const apmIndicesSavedObject = await getApmIndicesSavedObject( - context.core.savedObjects.client + savedObjectsClient ); - const apmIndicesConfig = getApmIndicesConfig(context.config); + const apmIndicesConfig = getApmIndicesConfig(config); return merge({}, apmIndicesConfig, apmIndicesSavedObject); } catch (error) { - return getApmIndicesConfig(context.config); + return getApmIndicesConfig(config); } } diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts index b66eb05f6eda5..a69fba52be3f0 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/apm_indices.ts @@ -26,7 +26,10 @@ export const apmIndicesRoute = createRoute(() => ({ method: 'GET', path: '/api/apm/settings/apm-indices', handler: async ({ context }) => { - return await getApmIndices(context); + return await getApmIndices({ + savedObjectsClient: context.core.savedObjects.client, + config: context.config + }); } })); diff --git a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts index 5b9389a073002..ace61e13193c8 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts @@ -5,7 +5,6 @@ */ import * as rt from 'io-ts'; -import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; export const InfraMetadataNodeTypeRT = rt.keyof({ host: null, @@ -67,6 +66,7 @@ export const InfraMetadataInfoRT = rt.partial({ }); const InfraMetadataRequiredRT = rt.type({ + id: rt.string, name: rt.string, features: rt.array(InfraMetadataFeatureRT), }); @@ -81,8 +81,6 @@ export type InfraMetadata = rt.TypeOf; export type InfraMetadataRequest = rt.TypeOf; -export type InfraMetadataWrappedRequest = InfraWrappableRequest; - export type InfraMetadataFeature = rt.TypeOf; export type InfraMetadataInfo = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts b/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts index 607d71654032e..46aab881bce4c 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/node_details_api.ts @@ -6,7 +6,6 @@ import * as rt from 'io-ts'; import { InventoryMetricRT, ItemTypeRT } from '../inventory_models/types'; -import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; import { InfraTimerangeInputRT } from './snapshot_api'; const NodeDetailsDataPointRT = rt.intersection([ @@ -53,6 +52,4 @@ export const NodeDetailsRequestRT = rt.intersection([ // export type NodeDetailsRequest = InfraWrappableRequest; export type NodeDetailsRequest = rt.TypeOf; -export type NodeDetailsWrappedRequest = InfraWrappableRequest; - export type NodeDetailsMetricDataResponse = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts index 24ca0fed73338..3e6aec4bad972 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/snapshot_api.ts @@ -5,7 +5,6 @@ */ import * as rt from 'io-ts'; -import { InfraWrappableRequest } from '../../server/lib/adapters/framework'; import { SnapshotMetricTypeRT, ItemTypeRT } from '../inventory_models/types'; export const SnapshotNodePathRT = rt.intersection([ @@ -64,6 +63,5 @@ export const SnapshotRequestRT = rt.intersection([ ]); export type SnapshotRequest = rt.TypeOf; -export type SnapshotWrappedRequest = InfraWrappableRequest; export type SnapshotNode = rt.TypeOf; export type SnapshotNodeResponse = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/index.ts b/x-pack/legacy/plugins/infra/index.ts index 9bf679fb5ff80..dbf1f4ad61de3 100644 --- a/x-pack/legacy/plugins/infra/index.ts +++ b/x-pack/legacy/plugins/infra/index.ts @@ -7,9 +7,14 @@ import { i18n } from '@kbn/i18n'; import JoiNamespace from 'joi'; import { resolve } from 'path'; - -import { getConfigSchema, initServerWithKibana } from './server/kibana.index'; +import { PluginInitializerContext } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import KbnServer from 'src/legacy/server/kbn_server'; +import { getConfigSchema } from './server/kibana.index'; import { savedObjectMappings } from './server/saved_objects'; +import { plugin, InfraServerPluginDeps } from './server/new_platform_index'; +import { InfraSetup } from '../../../plugins/infra/server'; +import { APMPluginContract } from '../../../plugins/apm/server/plugin'; const APP_ID = 'infra'; const logsSampleDataLinkLabel = i18n.translate('xpack.infra.sampleDataLinkLabel', { @@ -70,9 +75,52 @@ export function infra(kibana: any) { config(Joi: typeof JoiNamespace) { return getConfigSchema(Joi); }, - init(server: any) { - initServerWithKibana(server); - server.addAppLinksToSampleDataset('logs', [ + init(legacyServer: any) { + const { newPlatform } = legacyServer as KbnServer; + const { core, plugins } = newPlatform.setup; + + const infraSetup = (plugins.infra as unknown) as InfraSetup; // chef's kiss + + const initContext = ({ + config: infraSetup.__legacy.config, + } as unknown) as PluginInitializerContext; + // NP_TODO: Use real types from the other plugins as they are migrated + const pluginDeps: InfraServerPluginDeps = { + usageCollection: plugins.usageCollection as UsageCollectionSetup, + indexPatterns: { + indexPatternsServiceFactory: legacyServer.indexPatternsServiceFactory, + }, + metrics: legacyServer.plugins.metrics, + spaces: plugins.spaces, + features: plugins.features, + // NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that + // originate from the New Platform router (and are very different to the old request object). + // Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just + // the requestContext, this can be removed. + ___legacy: { + tsvb: { + elasticsearch: legacyServer.plugins.elasticsearch, + __internals: legacyServer.newPlatform.__internals, + }, + }, + apm: plugins.apm as APMPluginContract, + }; + + const infraPluginInstance = plugin(initContext); + infraPluginInstance.setup(core, pluginDeps); + + // NP_TODO: EVERYTHING BELOW HERE IS LEGACY + + const libs = infraPluginInstance.getLibs(); + + // NP_TODO how do we replace this? Answer: return from setup function. + legacyServer.expose( + 'defineInternalSourceConfiguration', + libs.sources.defineInternalSourceConfiguration.bind(libs.sources) + ); + + // NP_TODO: How do we move this to new platform? + legacyServer.addAppLinksToSampleDataset('logs', [ { path: `/app/${APP_ID}#/logs`, label: logsSampleDataLinkLabel, diff --git a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts index d70a42473b710..f91b40815a3ae 100644 --- a/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/public/lib/adapters/framework/kibana_framework_adapter.ts @@ -24,7 +24,7 @@ import { const ROOT_ELEMENT_ID = 'react-infra-root'; const BREADCRUMBS_ELEMENT_ID = 'react-infra-breadcrumbs'; -export class InfraKibanaFrameworkAdapter implements InfraFrameworkAdapter { +export class KibanaFramework implements InfraFrameworkAdapter { public appState: object; public kbnVersion?: string; public timezone?: string; diff --git a/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts b/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts index 086691e665b03..9b0beb3ad519c 100644 --- a/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts +++ b/x-pack/legacy/plugins/infra/public/lib/compose/kibana_compose.ts @@ -20,7 +20,7 @@ import { HttpLink } from 'apollo-link-http'; import { withClientState } from 'apollo-link-state'; import { InfraFrontendLibs } from '../lib'; import introspectionQueryResultData from '../../graphql/introspection.json'; -import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; export function compose(): InfraFrontendLibs { @@ -57,7 +57,7 @@ export function compose(): InfraFrontendLibs { const infraModule = uiModules.get('app/infa'); - const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider); + const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider); const libs: InfraFrontendLibs = { apolloClient, diff --git a/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts b/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts index 14fd66d378121..1e0b2f079497d 100644 --- a/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts +++ b/x-pack/legacy/plugins/infra/public/lib/compose/testing_compose.ts @@ -17,7 +17,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory'; import ApolloClient from 'apollo-client'; import { SchemaLink } from 'apollo-link-schema'; import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools'; -import { InfraKibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaObservableApiAdapter } from '../adapters/observable_api/kibana_observable_api'; import { InfraFrontendLibs } from '../lib'; @@ -27,7 +27,7 @@ export function compose(): InfraFrontendLibs { basePath: chrome.getBasePath(), xsrfToken: chrome.getXsrfToken(), }); - const framework = new InfraKibanaFrameworkAdapter(infraModule, uiRoutes, timezoneProvider); + const framework = new KibanaFramework(infraModule, uiRoutes, timezoneProvider); const typeDefs = ` Query {} `; diff --git a/x-pack/legacy/plugins/infra/server/features.ts b/x-pack/legacy/plugins/infra/server/features.ts new file mode 100644 index 0000000000000..fc20813c777b6 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/features.ts @@ -0,0 +1,65 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const METRICS_FEATURE = { + id: 'infrastructure', + name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { + defaultMessage: 'Infrastructure', + }), + icon: 'infraApp', + navLinkId: 'infra:home', + app: ['infra', 'kibana'], + catalogue: ['infraops'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: ['index-pattern'], + }, + ui: ['show', 'configureSource', 'save'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['infrastructure-ui-source', 'index-pattern'], + }, + ui: ['show'], + }, + }, +}; + +export const LOGS_FEATURE = { + id: 'logs', + name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { + defaultMessage: 'Logs', + }), + icon: 'loggingApp', + navLinkId: 'infra:logs', + app: ['infra', 'kibana'], + catalogue: ['infralogging'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: [], + }, + ui: ['show', 'configureSource', 'save'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['infrastructure-ui-source'], + }, + ui: ['show'], + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/server/infra_server.ts b/x-pack/legacy/plugins/infra/server/infra_server.ts index edccf5f413ab4..845e54e18c7c5 100644 --- a/x-pack/legacy/plugins/infra/server/infra_server.ts +++ b/x-pack/legacy/plugins/infra/server/infra_server.ts @@ -30,7 +30,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { typeDefs: schemas, }); - libs.framework.registerGraphQLEndpoint('/api/infra/graphql', schema); + libs.framework.registerGraphQLEndpoint('/graphql', schema); initIpToHostName(libs); initLogAnalysisGetLogEntryRateRoute(libs); diff --git a/x-pack/legacy/plugins/infra/server/kibana.index.ts b/x-pack/legacy/plugins/infra/server/kibana.index.ts index 91bcd6be95a75..b4301b3edf367 100644 --- a/x-pack/legacy/plugins/infra/server/kibana.index.ts +++ b/x-pack/legacy/plugins/infra/server/kibana.index.ts @@ -4,97 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import { Server } from 'hapi'; import JoiNamespace from 'joi'; -import { initInfraServer } from './infra_server'; -import { compose } from './lib/compose/kibana'; -import { UsageCollector } from './usage/usage_collector'; -import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; -import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; -export const initServerWithKibana = (kbnServer: Server) => { - const { usageCollection } = kbnServer.newPlatform.setup.plugins; - const libs = compose(kbnServer); - initInfraServer(libs); - - kbnServer.expose( - 'defineInternalSourceConfiguration', - libs.sources.defineInternalSourceConfiguration.bind(libs.sources) - ); - - // Register a function with server to manage the collection of usage stats - UsageCollector.registerUsageCollector(usageCollection); - - const xpackMainPlugin = kbnServer.plugins.xpack_main; - xpackMainPlugin.registerFeature({ - id: 'infrastructure', - name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { - defaultMessage: 'Metrics', - }), - icon: 'metricsApp', - navLinkId: 'infra:home', - app: ['infra', 'kibana'], - catalogue: ['infraops'], - privileges: { - all: { - api: ['infra'], - savedObject: { - all: [ - 'infrastructure-ui-source', - inventoryViewSavedObjectType, - metricsExplorerViewSavedObjectType, - ], - read: ['index-pattern'], - }, - ui: ['show', 'configureSource', 'save'], - }, - read: { - api: ['infra'], - savedObject: { - all: [], - read: [ - 'infrastructure-ui-source', - 'index-pattern', - inventoryViewSavedObjectType, - metricsExplorerViewSavedObjectType, - ], - }, - ui: ['show'], - }, - }, - }); - - xpackMainPlugin.registerFeature({ - id: 'logs', - name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { - defaultMessage: 'Logs', - }), - icon: 'logsApp', - navLinkId: 'infra:logs', - app: ['infra', 'kibana'], - catalogue: ['infralogging'], - privileges: { - all: { - api: ['infra'], - savedObject: { - all: ['infrastructure-ui-source'], - read: [], - }, - ui: ['show', 'configureSource', 'save'], - }, - read: { - api: ['infra'], - savedObject: { - all: [], - read: ['infrastructure-ui-source'], - }, - ui: ['show'], - }, - }, - }); -}; +export interface KbnServer extends Server { + usage: any; +} +// NP_TODO: this is only used in the root index file AFAICT, can remove after migrating to NP export const getConfigSchema = (Joi: typeof JoiNamespace) => { const InfraDefaultSourceConfigSchema = Joi.object({ metricAlias: Joi.string(), @@ -111,6 +28,7 @@ export const getConfigSchema = (Joi: typeof JoiNamespace) => { }), }); + // NP_TODO: make sure this is all represented in the NP config schema const InfraRootConfigSchema = Joi.object({ enabled: Joi.boolean().default(true), query: Joi.object({ diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts deleted file mode 100644 index b0856cf3da361..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/adapter_types.ts +++ /dev/null @@ -1,19 +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. - */ - -export interface InfraConfigurationAdapter< - Configuration extends InfraBaseConfiguration = InfraBaseConfiguration -> { - get(): Promise; -} - -export interface InfraBaseConfiguration { - enabled: boolean; - query: { - partitionSize: number; - partitionFactor: number; - }; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts deleted file mode 100644 index 4e09b5d0e9e2d..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export * from './adapter_types'; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts deleted file mode 100644 index 472fa72939565..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/inmemory_configuration_adapter.ts +++ /dev/null @@ -1,16 +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 { InfraBaseConfiguration, InfraConfigurationAdapter } from './adapter_types'; - -export class InfraInmemoryConfigurationAdapter - implements InfraConfigurationAdapter { - constructor(private readonly configuration: Configuration) {} - - public async get() { - return this.configuration; - } -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts deleted file mode 100644 index 4d87878e9aa87..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.test.ts +++ /dev/null @@ -1,40 +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 { InfraKibanaConfigurationAdapter } from './kibana_configuration_adapter'; - -describe('the InfraKibanaConfigurationAdapter', () => { - test('queries the xpack.infra configuration of the server', async () => { - const mockConfig = { - get: jest.fn(), - }; - - const configurationAdapter = new InfraKibanaConfigurationAdapter({ - config: () => mockConfig, - }); - - await configurationAdapter.get(); - - expect(mockConfig.get).toBeCalledWith('xpack.infra'); - }); - - test('applies the query defaults', async () => { - const configurationAdapter = new InfraKibanaConfigurationAdapter({ - config: () => ({ - get: () => ({}), - }), - }); - - const configuration = await configurationAdapter.get(); - - expect(configuration).toMatchObject({ - query: { - partitionSize: expect.any(Number), - partitionFactor: expect.any(Number), - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts deleted file mode 100644 index d3699a4820cf0..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/configuration/kibana_configuration_adapter.ts +++ /dev/null @@ -1,73 +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 Joi from 'joi'; - -import { InfraBaseConfiguration, InfraConfigurationAdapter } from './adapter_types'; - -export class InfraKibanaConfigurationAdapter implements InfraConfigurationAdapter { - private readonly server: ServerWithConfig; - - constructor(server: any) { - if (!isServerWithConfig(server)) { - throw new Error('Failed to find configuration on server.'); - } - - this.server = server; - } - - public async get() { - const config = this.server.config(); - - if (!isKibanaConfiguration(config)) { - throw new Error('Failed to access configuration of server.'); - } - - const configuration = config.get('xpack.infra') || {}; - const configurationWithDefaults: InfraBaseConfiguration = { - enabled: true, - query: { - partitionSize: 75, - partitionFactor: 1.2, - ...(configuration.query || {}), - }, - ...configuration, - }; - - // we assume this to be the configuration because Kibana would have already validated it - return configurationWithDefaults; - } -} - -interface ServerWithConfig { - config(): any; -} - -function isServerWithConfig(maybeServer: any): maybeServer is ServerWithConfig { - return ( - Joi.validate( - maybeServer, - Joi.object({ - config: Joi.func().required(), - }).unknown() - ).error === null - ); -} - -interface KibanaConfiguration { - get(key: string): any; -} - -function isKibanaConfiguration(maybeConfiguration: any): maybeConfiguration is KibanaConfiguration { - return ( - Joi.validate( - maybeConfiguration, - Joi.object({ - get: Joi.func().required(), - }).unknown() - ).error === null - ); -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts index 66081e60e7e10..3aaa23b378096 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/adapter_types.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraFrameworkRequest } from '../framework'; +import { RequestHandlerContext } from 'src/core/server'; export interface FieldsAdapter { getIndexFields( - req: InfraFrameworkRequest, + requestContext: RequestHandlerContext, indices: string, timefield: string ): Promise; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts index a6881a05f6f93..01306901e9caa 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/fields/framework_fields_adapter.ts @@ -6,11 +6,9 @@ import { startsWith, uniq, first } from 'lodash'; import { idx } from '@kbn/elastic-idx'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraDatabaseSearchResponse, -} from '../framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { InfraDatabaseSearchResponse } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { FieldsAdapter, IndexFieldDescriptor } from './adapter_types'; import { getAllowedListForPrefix } from '../../../../common/ecs_allowed_list'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; @@ -31,22 +29,26 @@ interface DataSetResponse { } export class FrameworkFieldsAdapter implements FieldsAdapter { - private framework: InfraBackendFrameworkAdapter; + private framework: KibanaFramework; - constructor(framework: InfraBackendFrameworkAdapter) { + constructor(framework: KibanaFramework) { this.framework = framework; } public async getIndexFields( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, indices: string, timefield: string ): Promise { - const indexPatternsService = this.framework.getIndexPatternsService(request); + const indexPatternsService = this.framework.getIndexPatternsService(requestContext); const response = await indexPatternsService.getFieldsForWildcard({ pattern: indices, }); - const { dataSets, modules } = await this.getDataSetsAndModules(request, indices, timefield); + const { dataSets, modules } = await this.getDataSetsAndModules( + requestContext, + indices, + timefield + ); const allowedList = modules.reduce( (acc, name) => uniq([...acc, ...getAllowedListForPrefix(name)]), [] as string[] @@ -59,7 +61,7 @@ export class FrameworkFieldsAdapter implements FieldsAdapter { } private async getDataSetsAndModules( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, indices: string, timefield: string ): Promise<{ dataSets: string[]; modules: string[] }> { @@ -109,7 +111,7 @@ export class FrameworkFieldsAdapter implements FieldsAdapter { const buckets = await getAllCompositeData( this.framework, - request, + requestContext, params, bucketSelector, handleAfterKey diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 63fded49d8222..625607c098028 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -4,91 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchResponse } from 'elasticsearch'; -import { GraphQLSchema } from 'graphql'; -import { Lifecycle, ResponseToolkit, RouteOptions } from 'hapi'; -import { Legacy } from 'kibana'; - -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { JsonObject } from '../../../../common/typed_json'; -import { TSVBMetricModel } from '../../../../common/inventory_models/types'; - -export const internalInfraFrameworkRequest = Symbol('internalInfraFrameworkRequest'); - -/* eslint-disable @typescript-eslint/unified-signatures */ -export interface InfraBackendFrameworkAdapter { - version: string; - exposeStaticDir(urlPath: string, dir: string): void; - registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void; - registerRoute( - route: InfraFrameworkRouteOptions - ): void; - callWithRequest( - req: InfraFrameworkRequest, - method: 'search', - options?: object - ): Promise>; - callWithRequest( - req: InfraFrameworkRequest, - method: 'msearch', - options?: object - ): Promise>; - callWithRequest( - req: InfraFrameworkRequest, - method: 'fieldCaps', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'indices.existsAlias', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'indices.getAlias', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'indices.get', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: 'ml.getBuckets', - options?: object - ): Promise; - callWithRequest( - req: InfraFrameworkRequest, - method: string, - options?: object - ): Promise; - getIndexPatternsService(req: InfraFrameworkRequest): Legacy.IndexPatternsService; - getSavedObjectsService(): Legacy.SavedObjectsService; - getSpaceId(request: InfraFrameworkRequest): string; - makeTSVBRequest( - req: InfraFrameworkRequest, - model: TSVBMetricModel, - timerange: { min: number; max: number }, - filters: JsonObject[] - ): Promise; - config(req: InfraFrameworkRequest): KibanaConfig; -} -/* eslint-enable @typescript-eslint/unified-signatures */ - -export interface InfraFrameworkRequest< - InternalRequest extends InfraWrappableRequest = InfraWrappableRequest -> { - [internalInfraFrameworkRequest]: InternalRequest; - payload: InternalRequest['payload']; - params: InternalRequest['params']; - query: InternalRequest['query']; -} - -export interface InfraWrappableRequest { - payload: Payload; - params: Params; - query: Query; +import { SearchResponse, GenericParams } from 'elasticsearch'; +import { Lifecycle } from 'hapi'; +import { ObjectType } from '@kbn/config-schema'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { RouteMethod, RouteConfig } from '../../../../../../../../src/core/server'; +import { APMPluginContract } from '../../../../../../../plugins/apm/server/plugin'; + +// NP_TODO: Compose real types from plugins we depend on, no "any" +export interface InfraServerPluginDeps { + usageCollection: UsageCollectionSetup; + spaces: any; + metrics: { + getVisData: any; + }; + indexPatterns: { + indexPatternsServiceFactory: any; + }; + features: any; + apm: APMPluginContract; + ___legacy: any; +} + +export interface CallWithRequestParams extends GenericParams { + max_concurrent_shard_requests?: number; + name?: string; + index?: string; + ignore_unavailable?: boolean; + allow_no_indices?: boolean; + size?: number; + terminate_after?: number; + fields?: string; } export type InfraResponse = Lifecycle.ReturnValue; @@ -98,22 +44,6 @@ export interface InfraFrameworkPluginOptions { options: any; } -export interface InfraFrameworkRouteOptions< - RouteRequest extends InfraWrappableRequest, - RouteResponse extends InfraResponse -> { - path: string; - method: string | string[]; - vhost?: string; - handler: InfraFrameworkRouteHandler; - options?: Pick>; -} - -export type InfraFrameworkRouteHandler< - RouteRequest extends InfraWrappableRequest, - RouteResponse extends InfraResponse -> = (request: InfraFrameworkRequest, h: ResponseToolkit) => RouteResponse; - export interface InfraDatabaseResponse { took: number; timeout: boolean; @@ -235,3 +165,12 @@ export interface InfraTSVBSeries { } export type InfraTSVBDataPoint = [number, number]; + +export type InfraRouteConfig< + params extends ObjectType, + query extends ObjectType, + body extends ObjectType, + method extends RouteMethod +> = { + method: RouteMethod; +} & RouteConfig; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts deleted file mode 100644 index da858217468f1..0000000000000 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/apollo_server_hapi.ts +++ /dev/null @@ -1,117 +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 * as GraphiQL from 'apollo-server-module-graphiql'; -import Boom from 'boom'; -import { Plugin, Request, ResponseToolkit, RouteOptions, Server } from 'hapi'; - -import { GraphQLOptions, runHttpQuery } from 'apollo-server-core'; - -export type HapiOptionsFunction = (req: Request) => GraphQLOptions | Promise; - -export interface HapiGraphQLPluginOptions { - path: string; - vhost?: string; - route?: RouteOptions; - graphqlOptions: GraphQLOptions | HapiOptionsFunction; -} - -export const graphqlHapi: Plugin = { - name: 'graphql', - register: (server: Server, options: HapiGraphQLPluginOptions) => { - if (!options || !options.graphqlOptions) { - throw new Error('Apollo Server requires options.'); - } - - server.route({ - options: options.route || {}, - handler: async (request: Request, h: ResponseToolkit) => { - try { - const query = - request.method === 'post' - ? (request.payload as Record) - : (request.query as Record); - - const gqlResponse = await runHttpQuery([request], { - method: request.method.toUpperCase(), - options: options.graphqlOptions, - query, - }); - - return h.response(gqlResponse).type('application/json'); - } catch (error) { - if ('HttpQueryError' !== error.name) { - const queryError = Boom.boomify(error); - - queryError.output.payload.message = error.message; - - return queryError; - } - - if (error.isGraphQLError === true) { - return h - .response(error.message) - .code(error.statusCode) - .type('application/json'); - } - - const genericError = new Boom(error.message, { statusCode: error.statusCode }); - - if (error.headers) { - Object.keys(error.headers).forEach(header => { - genericError.output.headers[header] = error.headers[header]; - }); - } - - // Boom hides the error when status code is 500 - - genericError.output.payload.message = error.message; - - throw genericError; - } - }, - method: ['GET', 'POST'], - path: options.path || '/graphql', - vhost: options.vhost || undefined, - }); - }, -}; - -export type HapiGraphiQLOptionsFunction = ( - req?: Request -) => GraphiQL.GraphiQLData | Promise; - -export interface HapiGraphiQLPluginOptions { - path: string; - - route?: any; - - graphiqlOptions: GraphiQL.GraphiQLData | HapiGraphiQLOptionsFunction; -} - -export const graphiqlHapi: Plugin = { - name: 'graphiql', - register: (server: Server, options: HapiGraphiQLPluginOptions) => { - if (!options || !options.graphiqlOptions) { - throw new Error('Apollo Server GraphiQL requires options.'); - } - - server.route({ - options: options.route || {}, - handler: async (request: Request, h: ResponseToolkit) => { - const graphiqlString = await GraphiQL.resolveGraphiQLString( - request.query, - options.graphiqlOptions, - request - ); - - return h.response(graphiqlString).type('text/html'); - }, - method: 'GET', - path: options.path || '/graphiql', - }); - }, -}; diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index e96f1687bbb2e..19121d92f02c9 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -4,116 +4,207 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/array-type */ + import { GenericParams } from 'elasticsearch'; import { GraphQLSchema } from 'graphql'; import { Legacy } from 'kibana'; - -import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { get } from 'lodash'; +import { runHttpQuery } from 'apollo-server-core'; +import { schema, TypeOf, ObjectType } from '@kbn/config-schema'; import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraFrameworkRouteOptions, - InfraResponse, + InfraRouteConfig, InfraTSVBResponse, - InfraWrappableRequest, - internalInfraFrameworkRequest, + InfraServerPluginDeps, + CallWithRequestParams, + InfraDatabaseSearchResponse, + InfraDatabaseMultiResponse, + InfraDatabaseFieldCapsResponse, + InfraDatabaseGetIndicesResponse, + InfraDatabaseGetIndicesAliasResponse, } from './adapter_types'; -import { - graphiqlHapi, - graphqlHapi, - HapiGraphiQLPluginOptions, - HapiGraphQLPluginOptions, -} from './apollo_server_hapi'; import { TSVBMetricModel } from '../../../../common/inventory_models/types'; +import { + CoreSetup, + IRouter, + KibanaRequest, + RequestHandlerContext, + KibanaResponseFactory, + RouteMethod, +} from '../../../../../../../../src/core/server'; +import { RequestHandler } from '../../../../../../../../src/core/server'; +import { InfraConfig } from '../../../../../../../plugins/infra/server'; -interface CallWithRequestParams extends GenericParams { - max_concurrent_shard_requests?: number; -} - -export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFrameworkAdapter { - public version: string; +export class KibanaFramework { + public router: IRouter; + private core: CoreSetup; + public plugins: InfraServerPluginDeps; - constructor(private server: Legacy.Server) { - this.version = server.config().get('pkg.version'); + constructor(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { + this.router = core.http.createRouter(); + this.core = core; + this.plugins = plugins; } - public config(req: InfraFrameworkRequest): KibanaConfig { - const internalRequest = req[internalInfraFrameworkRequest]; - return internalRequest.server.config(); + public registerRoute< + params extends ObjectType = any, + query extends ObjectType = any, + body extends ObjectType = any, + method extends RouteMethod = any + >( + config: InfraRouteConfig, + handler: RequestHandler + ) { + const defaultOptions = { + tags: ['access:infra'], + }; + const routeConfig = { + path: config.path, + validate: config.validate, + // Currently we have no use of custom options beyond tags, this can be extended + // beyond defaultOptions if it's needed. + options: defaultOptions, + }; + switch (config.method) { + case 'get': + this.router.get(routeConfig, handler); + break; + case 'post': + this.router.post(routeConfig, handler); + break; + case 'delete': + this.router.delete(routeConfig, handler); + break; + case 'put': + this.router.put(routeConfig, handler); + break; + } } - public exposeStaticDir(urlPath: string, dir: string): void { - this.server.route({ - handler: { - directory: { - path: dir, - }, - }, - method: 'GET', - path: urlPath, - }); - } + public registerGraphQLEndpoint(routePath: string, gqlSchema: GraphQLSchema) { + // These endpoints are validated by GraphQL at runtime and with GraphQL generated types + const body = schema.object({}, { allowUnknowns: true }); + type Body = TypeOf; - public registerGraphQLEndpoint(routePath: string, schema: GraphQLSchema): void { - this.server.register({ - options: { - graphqlOptions: (req: Legacy.Request) => ({ - context: { req: wrapRequest(req) }, - schema, - }), - path: routePath, - route: { - tags: ['access:infra'], - }, + const routeOptions = { + path: `/api/infra${routePath}`, + validate: { + body, }, - plugin: graphqlHapi, - }); - - this.server.register({ options: { - graphiqlOptions: request => ({ - endpointURL: request ? `${request.getBasePath()}${routePath}` : routePath, - passHeader: `'kbn-version': '${this.version}'`, - }), - path: `${routePath}/graphiql`, - route: { - tags: ['access:infra'], - }, + tags: ['access:infra'], }, - plugin: graphiqlHapi, - }); - } + }; + async function handler( + context: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + try { + const query = + request.route.method === 'post' + ? (request.body as Record) + : (request.query as Record); - public registerRoute< - RouteRequest extends InfraWrappableRequest, - RouteResponse extends InfraResponse - >(route: InfraFrameworkRouteOptions) { - const wrappedHandler = (request: any, h: Legacy.ResponseToolkit) => - route.handler(wrapRequest(request), h); - - this.server.route({ - handler: wrappedHandler, - options: route.options, - method: route.method, - path: route.path, - }); + const gqlResponse = await runHttpQuery([context, request], { + method: request.route.method.toUpperCase(), + options: (req: RequestHandlerContext, rawReq: KibanaRequest) => ({ + context: { req, rawReq }, + schema: gqlSchema, + }), + query, + }); + + return response.ok({ + body: gqlResponse, + headers: { + 'content-type': 'application/json', + }, + }); + } catch (error) { + const errorBody = { + message: error.message, + }; + + if ('HttpQueryError' !== error.name) { + return response.internalError({ + body: errorBody, + }); + } + + if (error.isGraphQLError === true) { + return response.customError({ + statusCode: error.statusCode, + body: errorBody, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + + const { headers = [], statusCode = 500 } = error; + return response.customError({ + statusCode, + headers, + body: errorBody, + }); + + // NP_TODO: Do we still need to re-throw this error in this case? if we do, can we + // still call the response.customError method to control the HTTP response? + // throw error; + } + } + this.router.post(routeOptions, handler); + this.router.get(routeOptions, handler); } - public async callWithRequest( - req: InfraFrameworkRequest, + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'search', + options?: CallWithRequestParams + ): Promise>; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'msearch', + options?: CallWithRequestParams + ): Promise>; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'fieldCaps', + options?: CallWithRequestParams + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: 'indices.existsAlias', + options?: CallWithRequestParams + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + method: 'indices.getAlias', + options?: object + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + method: 'indices.get' | 'ml.getBuckets', + options?: object + ): Promise; + callWithRequest( + requestContext: RequestHandlerContext, + endpoint: string, + options?: CallWithRequestParams + ): Promise; + + public async callWithRequest( + requestContext: RequestHandlerContext, endpoint: string, - params: CallWithRequestParams, - ...rest: any[] + params: CallWithRequestParams ) { - const internalRequest = req[internalInfraFrameworkRequest]; - const { elasticsearch } = internalRequest.server.plugins; - const { callWithRequest } = elasticsearch.getCluster('data'); - const includeFrozen = await internalRequest.getUiSettingsService().get('search:includeFrozen'); + const { elasticsearch, uiSettings } = requestContext.core; + + const includeFrozen = await uiSettings.client.get('search:includeFrozen'); if (endpoint === 'msearch') { - const maxConcurrentShardRequests = await internalRequest - .getUiSettingsService() - .get('courier:maxConcurrentShardRequests'); + const maxConcurrentShardRequests = await uiSettings.client.get( + 'courier:maxConcurrentShardRequests' + ); if (maxConcurrentShardRequests > 0) { params = { ...params, max_concurrent_shard_requests: maxConcurrentShardRequests }; } @@ -125,95 +216,79 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework } : {}; - const fields = await callWithRequest( - internalRequest, - endpoint, - { - ...params, - ...frozenIndicesParams, - }, - ...rest - ); - return fields; + return elasticsearch.dataClient.callAsCurrentUser(endpoint, { + ...params, + ...frozenIndicesParams, + }); } public getIndexPatternsService( - request: InfraFrameworkRequest + requestContext: RequestHandlerContext ): Legacy.IndexPatternsService { - return this.server.indexPatternsServiceFactory({ + return this.plugins.indexPatterns.indexPatternsServiceFactory({ callCluster: async (method: string, args: [GenericParams], ...rest: any[]) => { - const fieldCaps = await this.callWithRequest( - request, - method, - { ...args, allowNoIndices: true } as GenericParams, - ...rest - ); + const fieldCaps = await this.callWithRequest(requestContext, method, { + ...args, + allowNoIndices: true, + } as GenericParams); return fieldCaps; }, }); } - public getSpaceId(request: InfraFrameworkRequest): string { - const spacesPlugin = this.server.plugins.spaces; + public getSpaceId(request: KibanaRequest): string { + const spacesPlugin = this.plugins.spaces; - if (spacesPlugin && typeof spacesPlugin.getSpaceId === 'function') { - return spacesPlugin.getSpaceId(request[internalInfraFrameworkRequest]); + if ( + spacesPlugin && + spacesPlugin.spacesService && + typeof spacesPlugin.spacesService.getSpaceId === 'function' + ) { + return spacesPlugin.spacesService.getSpaceId(request); } else { return 'default'; } } - public getSavedObjectsService() { - return this.server.savedObjects; - } - + // NP_TODO: This method needs to no longer require full KibanaRequest public async makeTSVBRequest( - req: InfraFrameworkRequest, + request: KibanaRequest, model: TSVBMetricModel, timerange: { min: number; max: number }, - filters: any[] - ) { - const internalRequest = req[internalInfraFrameworkRequest]; - const server = internalRequest.server; - const getVisData = get(server, 'plugins.metrics.getVisData'); + filters: any[], + requestContext: RequestHandlerContext + ): Promise { + const { getVisData } = this.plugins.metrics; if (typeof getVisData !== 'function') { throw new Error('TSVB is not available'); } - - // getBasePath returns randomized base path AND spaces path - const basePath = internalRequest.getBasePath(); - const url = `${basePath}/api/metrics/vis/data`; - + const url = this.core.http.basePath.prepend('/api/metrics/vis/data'); // For the following request we need a copy of the instnace of the internal request // but modified for our TSVB request. This will ensure all the instance methods // are available along with our overriden values - const request = Object.assign( - Object.create(Object.getPrototypeOf(internalRequest)), - internalRequest, - { - url, - method: 'POST', - payload: { - timerange, - panels: [model], - filters, + const requestCopy = Object.assign({}, request, { + url, + method: 'POST', + payload: { + timerange, + panels: [model], + filters, + }, + // NP_NOTE: [TSVB_GROUP] Huge hack to make TSVB (getVisData()) work with raw requests that + // originate from the New Platform router (and are very different to the old request object). + // Once TSVB has migrated over to NP, and can work with the new raw requests, or ideally just + // the requestContext, this can be removed. + server: { + plugins: { + elasticsearch: this.plugins.___legacy.tsvb.elasticsearch, }, - } - ); - const result = await getVisData(request); - return result as InfraTSVBResponse; + newPlatform: { + __internals: this.plugins.___legacy.tsvb.__internals, + }, + }, + getUiSettingsService: () => requestContext.core.uiSettings.client, + getSavedObjectsClient: () => requestContext.core.savedObjects.client, + }); + return getVisData(requestCopy); } } - -export function wrapRequest( - req: InternalRequest -): InfraFrameworkRequest { - const { params, payload, query } = req; - - return { - [internalInfraFrameworkRequest]: req, - params, - payload, - query, - }; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 547e74eecb67c..ec45171baa7b0 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -15,6 +15,7 @@ import zip from 'lodash/fp/zip'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity, constant } from 'fp-ts/lib/function'; +import { RequestHandlerContext } from 'src/core/server'; import { compareTimeKeys, isTimeKey, TimeKey } from '../../../../common/time'; import { JsonObject } from '../../../../common/typed_json'; import { @@ -24,8 +25,8 @@ import { LogSummaryBucket, } from '../../domains/log_entries_domain'; import { InfraSourceConfiguration } from '../../sources'; -import { InfraFrameworkRequest, SortedSearchHit } from '../framework'; -import { InfraBackendFrameworkAdapter } from '../framework'; +import { SortedSearchHit } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; const DAY_MILLIS = 24 * 60 * 60 * 1000; const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000, Infinity].map(days => days * DAY_MILLIS); @@ -39,10 +40,10 @@ interface LogItemHit { } export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { - constructor(private readonly framework: InfraBackendFrameworkAdapter) {} + constructor(private readonly framework: KibanaFramework) {} public async getAdjacentLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -64,7 +65,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } const documentsInInterval = await this.getLogEntryDocumentsBetween( - request, + requestContext, sourceConfiguration, fields, intervalStart, @@ -82,7 +83,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } public async getContainedLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -91,7 +92,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { highlightQuery?: LogEntryQuery ): Promise { const documents = await this.getLogEntryDocumentsBetween( - request, + requestContext, sourceConfiguration, fields, start.time, @@ -106,7 +107,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } public async getContainedLogSummaryBuckets( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, start: number, end: number, @@ -165,7 +166,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { }, }; - const response = await this.framework.callWithRequest(request, 'search', query); + const response = await this.framework.callWithRequest(requestContext, 'search', query); return pipe( LogSummaryResponseRuntimeType.decode(response), @@ -179,12 +180,12 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } public async getLogItem( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, id: string, sourceConfiguration: InfraSourceConfiguration ) { const search = (searchOptions: object) => - this.framework.callWithRequest(request, 'search', searchOptions); + this.framework.callWithRequest(requestContext, 'search', searchOptions); const params = { index: sourceConfiguration.logAlias, @@ -212,7 +213,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { } private async getLogEntryDocumentsBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: number, @@ -298,7 +299,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { }; const response = await this.framework.callWithRequest( - request, + requestContext, 'search', query ); diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts index adb8c811ed57d..acd7a2528bb42 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/adapter_types.ts @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext, KibanaRequest } from 'src/core/server'; import { InfraMetric, InfraMetricData, InfraNodeType, InfraTimerangeInput, } from '../../../graphql/types'; - import { InfraSourceConfiguration } from '../../sources'; -import { InfraFrameworkRequest } from '../framework'; export interface InfraMetricsRequestOptions { nodeIds: { @@ -27,8 +26,9 @@ export interface InfraMetricsRequestOptions { export interface InfraMetricsAdapter { getMetrics( - req: InfraFrameworkRequest, - options: InfraMetricsRequestOptions + requestContext: RequestHandlerContext, + options: InfraMetricsRequestOptions, + request: KibanaRequest // NP_TODO: temporarily needed until metrics getVisData no longer needs full request ): Promise; } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index 331abd4ffb35a..db3c516841cd4 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -6,10 +6,9 @@ import { i18n } from '@kbn/i18n'; import { flatten, get } from 'lodash'; - -import Boom from 'boom'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { InfraMetric, InfraMetricData, InfraNodeType } from '../../../graphql/types'; -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from './adapter_types'; import { checkValidNode } from './lib/check_valid_node'; import { metrics } from '../../../../common/inventory_models'; @@ -17,15 +16,16 @@ import { TSVBMetricModelCreator } from '../../../../common/inventory_models/type import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; export class KibanaMetricsAdapter implements InfraMetricsAdapter { - private framework: InfraBackendFrameworkAdapter; + private framework: KibanaFramework; - constructor(framework: InfraBackendFrameworkAdapter) { + constructor(framework: KibanaFramework) { this.framework = framework; } public async getMetrics( - req: InfraFrameworkRequest, - options: InfraMetricsRequestOptions + requestContext: RequestHandlerContext, + options: InfraMetricsRequestOptions, + rawRequest: KibanaRequest // NP_TODO: Temporarily needed until metrics getVisData no longer needs full request ): Promise { const fields = { [InfraNodeType.host]: options.sourceConfiguration.fields.host, @@ -35,11 +35,11 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`; const nodeField = fields[options.nodeType]; const search = (searchOptions: object) => - this.framework.callWithRequest<{}, Aggregation>(req, 'search', searchOptions); + this.framework.callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions); const validNode = await checkValidNode(search, indexPattern, nodeField, options.nodeIds.nodeId); if (!validNode) { - throw Boom.notFound( + throw new Error( i18n.translate('xpack.infra.kibanaMetrics.nodeDoesNotExistErrorMessage', { defaultMessage: '{nodeId} does not exist.', values: { @@ -50,7 +50,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { } const requests = options.metrics.map(metricId => - this.makeTSVBRequest(metricId, options, req, nodeField) + this.makeTSVBRequest(metricId, options, rawRequest, nodeField, requestContext) ); return Promise.all(requests) @@ -92,12 +92,13 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { async makeTSVBRequest( metricId: InfraMetric, options: InfraMetricsRequestOptions, - req: InfraFrameworkRequest, - nodeField: string + req: KibanaRequest, + nodeField: string, + requestContext: RequestHandlerContext ) { const createTSVBModel = get(metrics, ['tsvb', metricId]) as TSVBMetricModelCreator | undefined; if (!createTSVBModel) { - throw Boom.badRequest( + throw new Error( i18n.translate('xpack.infra.metrics.missingTSVBModelError', { defaultMessage: 'The TSVB model for {metricId} does not exist for {nodeType}', values: { @@ -121,7 +122,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { ); const calculatedInterval = await calculateMetricInterval( this.framework, - req, + requestContext, { indexPattern: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, timestampField: options.sourceConfiguration.fields.timestamp, @@ -135,7 +136,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { } if (model.id_type === 'cloud' && !options.nodeIds.cloudId) { - throw Boom.badRequest( + throw new Error( i18n.translate('xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage', { defaultMessage: 'Model for {metricId} requires a cloudId, but none was given for {nodeId}.', @@ -152,6 +153,6 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { ? [{ match: { [model.map_field_to]: id } }] : [{ match: { [nodeField]: id } }]; - return this.framework.makeTSVBRequest(req, model, timerange, filters); + return this.framework.makeTSVBRequest(req, model, timerange, filters, requestContext); } } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts index e66da3f3fa6cb..635f6ff9762c5 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/source_status/elasticsearch_source_status_adapter.ts @@ -4,26 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'src/core/server'; import { InfraSourceStatusAdapter } from '../../source_status'; -import { - InfraBackendFrameworkAdapter, - InfraDatabaseGetIndicesResponse, - InfraFrameworkRequest, -} from '../framework'; +import { InfraDatabaseGetIndicesResponse } from '../framework'; +import { KibanaFramework } from '../framework/kibana_framework_adapter'; export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusAdapter { - constructor(private readonly framework: InfraBackendFrameworkAdapter) {} + constructor(private readonly framework: KibanaFramework) {} - public async getIndexNames(request: InfraFrameworkRequest, aliasName: string) { + public async getIndexNames(requestContext: RequestHandlerContext, aliasName: string) { const indexMaps = await Promise.all([ this.framework - .callWithRequest(request, 'indices.getAlias', { + .callWithRequest(requestContext, 'indices.getAlias', { name: aliasName, filterPath: '*.settings.index.uuid', // to keep the response size as small as possible }) .catch(withDefaultIfNotFound({})), this.framework - .callWithRequest(request, 'indices.get', { + .callWithRequest(requestContext, 'indices.get', { index: aliasName, filterPath: '*.settings.index.uuid', // to keep the response size as small as possible }) @@ -36,15 +34,15 @@ export class InfraElasticsearchSourceStatusAdapter implements InfraSourceStatusA ); } - public async hasAlias(request: InfraFrameworkRequest, aliasName: string) { - return await this.framework.callWithRequest(request, 'indices.existsAlias', { + public async hasAlias(requestContext: RequestHandlerContext, aliasName: string) { + return await this.framework.callWithRequest(requestContext, 'indices.existsAlias', { name: aliasName, }); } - public async hasIndices(request: InfraFrameworkRequest, indexNames: string) { + public async hasIndices(requestContext: RequestHandlerContext, indexNames: string) { return await this.framework - .callWithRequest(request, 'search', { + .callWithRequest(requestContext, 'search', { ignore_unavailable: true, allow_no_indices: true, index: indexNames, diff --git a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts index 215c41bcf6b7c..305841aa52d36 100644 --- a/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts +++ b/x-pack/legacy/plugins/infra/server/lib/compose/kibana.ts @@ -3,12 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { Server } from 'hapi'; - -import { InfraKibanaConfigurationAdapter } from '../adapters/configuration/kibana_configuration_adapter'; import { FrameworkFieldsAdapter } from '../adapters/fields/framework_fields_adapter'; -import { InfraKibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraKibanaLogEntriesAdapter } from '../adapters/log_entries/kibana_log_entries_adapter'; import { KibanaMetricsAdapter } from '../adapters/metrics/kibana_metrics_adapter'; import { InfraElasticsearchSourceStatusAdapter } from '../adapters/source_status'; @@ -20,13 +16,14 @@ import { InfraLogAnalysis } from '../log_analysis'; import { InfraSnapshot } from '../snapshot'; import { InfraSourceStatus } from '../source_status'; import { InfraSources } from '../sources'; +import { InfraConfig } from '../../../../../../plugins/infra/server'; +import { CoreSetup } from '../../../../../../../src/core/server'; +import { InfraServerPluginDeps } from '../adapters/framework/adapter_types'; -export function compose(server: Server): InfraBackendLibs { - const configuration = new InfraKibanaConfigurationAdapter(server); - const framework = new InfraKibanaBackendFrameworkAdapter(server); +export function compose(core: CoreSetup, config: InfraConfig, plugins: InfraServerPluginDeps) { + const framework = new KibanaFramework(core, config, plugins); const sources = new InfraSources({ - configuration, - savedObjects: framework.getSavedObjectsService(), + config, }); const sourceStatus = new InfraSourceStatus(new InfraElasticsearchSourceStatusAdapter(framework), { sources, @@ -34,6 +31,7 @@ export function compose(server: Server): InfraBackendLibs { const snapshot = new InfraSnapshot({ sources, framework }); const logAnalysis = new InfraLogAnalysis({ framework }); + // TODO: separate these out individually and do away with "domains" as a temporary group const domainLibs: InfraDomainLibs = { fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { sources, @@ -45,7 +43,7 @@ export function compose(server: Server): InfraBackendLibs { }; const libs: InfraBackendLibs = { - configuration, + configuration: config, // NP_TODO: Do we ever use this anywhere? framework, logAnalysis, snapshot, diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts index c5a3bbeb87449..a00c76216da4c 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/fields_domain.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'src/core/server'; import { InfraIndexField, InfraIndexType } from '../../graphql/types'; import { FieldsAdapter } from '../adapters/fields'; -import { InfraFrameworkRequest } from '../adapters/framework'; import { InfraSources } from '../sources'; export class InfraFieldsDomain { @@ -16,16 +16,19 @@ export class InfraFieldsDomain { ) {} public async getFields( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, indexType: InfraIndexType ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const includeMetricIndices = [InfraIndexType.ANY, InfraIndexType.METRICS].includes(indexType); const includeLogIndices = [InfraIndexType.ANY, InfraIndexType.LOGS].includes(indexType); const fields = await this.adapter.getIndexFields( - request, + requestContext, `${includeMetricIndices ? configuration.metricAlias : ''},${ includeLogIndices ? configuration.logAlias : '' }`, diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 0127f80b31357..597073b1e901f 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -7,6 +7,7 @@ import stringify from 'json-stable-stringify'; import { sortBy } from 'lodash'; +import { RequestHandlerContext } from 'src/core/server'; import { TimeKey } from '../../../../common/time'; import { JsonObject } from '../../../../common/typed_json'; import { @@ -16,7 +17,6 @@ import { InfraLogSummaryBucket, InfraLogSummaryHighlightBucket, } from '../../../graphql/types'; -import { InfraFrameworkRequest } from '../../adapters/framework'; import { InfraSourceConfiguration, InfraSources, @@ -40,7 +40,7 @@ export class InfraLogEntriesDomain { ) {} public async getLogEntriesAround( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, key: TimeKey, maxCountBefore: number, @@ -55,14 +55,17 @@ export class InfraLogEntriesDomain { }; } - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); const requiredFields = getRequiredFields(configuration, messageFormattingRules); const documentsBefore = await this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, key, @@ -80,7 +83,7 @@ export class InfraLogEntriesDomain { }; const documentsAfter = await this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, lastKeyBefore, @@ -101,20 +104,23 @@ export class InfraLogEntriesDomain { } public async getLogEntriesBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, startKey: TimeKey, endKey: TimeKey, filterQuery?: LogEntryQuery, highlightQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); const requiredFields = getRequiredFields(configuration, messageFormattingRules); const documents = await this.adapter.getContainedLogEntryDocuments( - request, + requestContext, configuration, requiredFields, startKey, @@ -129,7 +135,7 @@ export class InfraLogEntriesDomain { } public async getLogEntryHighlights( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, startKey: TimeKey, endKey: TimeKey, @@ -140,7 +146,10 @@ export class InfraLogEntriesDomain { }>, filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); @@ -158,7 +167,7 @@ export class InfraLogEntriesDomain { : highlightQuery; const [documentsBefore, documents, documentsAfter] = await Promise.all([ this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, startKey, @@ -168,7 +177,7 @@ export class InfraLogEntriesDomain { highlightQuery ), this.adapter.getContainedLogEntryDocuments( - request, + requestContext, configuration, requiredFields, startKey, @@ -177,7 +186,7 @@ export class InfraLogEntriesDomain { highlightQuery ), this.adapter.getAdjacentLogEntryDocuments( - request, + requestContext, configuration, requiredFields, endKey, @@ -203,16 +212,19 @@ export class InfraLogEntriesDomain { } public async getLogSummaryBucketsBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, start: number, end: number, bucketSize: number, filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets( - request, + requestContext, configuration, start, end, @@ -223,7 +235,7 @@ export class InfraLogEntriesDomain { } public async getLogSummaryHighlightBucketsBetween( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, start: number, end: number, @@ -231,7 +243,10 @@ export class InfraLogEntriesDomain { highlightQueries: string[], filterQuery?: LogEntryQuery ): Promise { - const { configuration } = await this.libs.sources.getSourceConfiguration(request, sourceId); + const { configuration } = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const messageFormattingRules = compileFormattingRules( getBuiltinRules(configuration.fields.message) ); @@ -248,7 +263,7 @@ export class InfraLogEntriesDomain { } : highlightQuery; const summaryBuckets = await this.adapter.getContainedLogSummaryBuckets( - request, + requestContext, configuration, start, end, @@ -266,11 +281,11 @@ export class InfraLogEntriesDomain { } public async getLogItem( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, id: string, sourceConfiguration: InfraSourceConfiguration ): Promise { - const document = await this.adapter.getLogItem(request, id, sourceConfiguration); + const document = await this.adapter.getLogItem(requestContext, id, sourceConfiguration); const defaultFields = [ { field: '_index', value: document._index }, { field: '_id', value: document._id }, @@ -300,7 +315,7 @@ interface LogItemHit { export interface LogEntriesAdapter { getAdjacentLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -311,7 +326,7 @@ export interface LogEntriesAdapter { ): Promise; getContainedLogEntryDocuments( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, fields: string[], start: TimeKey, @@ -321,7 +336,7 @@ export interface LogEntriesAdapter { ): Promise; getContainedLogSummaryBuckets( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, start: number, end: number, @@ -330,7 +345,7 @@ export interface LogEntriesAdapter { ): Promise; getLogItem( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, id: string, source: InfraSourceConfiguration ): Promise; diff --git a/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts b/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts index 862ca8b4c823f..5d7d54a6a2e50 100644 --- a/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts +++ b/x-pack/legacy/plugins/infra/server/lib/domains/metrics_domain.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { InfraMetricData } from '../../graphql/types'; -import { InfraFrameworkRequest } from '../adapters/framework/adapter_types'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from '../adapters/metrics/adapter_types'; export class InfraMetricsDomain { @@ -16,9 +16,10 @@ export class InfraMetricsDomain { } public async getMetrics( - req: InfraFrameworkRequest, - options: InfraMetricsRequestOptions + requestContext: RequestHandlerContext, + options: InfraMetricsRequestOptions, + rawRequest: KibanaRequest // NP_TODO: temporarily needed until metrics getVisData no longer needs full request ): Promise { - return await this.adapter.getMetrics(req, options); + return await this.adapter.getMetrics(requestContext, options, rawRequest); } } diff --git a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts index b436bb7e4fe58..46d32885600df 100644 --- a/x-pack/legacy/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/infra_types.ts @@ -5,8 +5,6 @@ */ import { InfraSourceConfiguration } from '../../public/graphql/types'; -import { InfraConfigurationAdapter } from './adapters/configuration'; -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from './adapters/framework'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; @@ -14,6 +12,15 @@ import { InfraLogAnalysis } from './log_analysis/log_analysis'; import { InfraSnapshot } from './snapshot'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; +import { InfraConfig } from '../../../../../plugins/infra/server'; +import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; + +// NP_TODO: We shouldn't need this context anymore but I am +// not sure how the graphql stuff uses it, so we can't remove it yet +export interface InfraContext { + req: any; + rawReq?: any; +} export interface InfraDomainLibs { fields: InfraFieldsDomain; @@ -22,8 +29,8 @@ export interface InfraDomainLibs { } export interface InfraBackendLibs extends InfraDomainLibs { - configuration: InfraConfigurationAdapter; - framework: InfraBackendFrameworkAdapter; + configuration: InfraConfig; + framework: KibanaFramework; logAnalysis: InfraLogAnalysis; snapshot: InfraSnapshot; sources: InfraSources; @@ -40,7 +47,3 @@ export interface InfraConfiguration { default: InfraSourceConfiguration; }; } - -export interface InfraContext { - req: InfraFrameworkRequest; -} diff --git a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts index d970a142c5c23..fac49a7980f26 100644 --- a/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts +++ b/x-pack/legacy/plugins/infra/server/lib/log_analysis/log_analysis.ts @@ -9,7 +9,7 @@ import { map, fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { getJobId } from '../../../common/log_analysis'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../adapters/framework'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { NoLogRateResultsIndexError } from './errors'; import { logRateModelPlotResponseRT, @@ -17,37 +17,38 @@ import { LogRateModelPlotBucket, CompositeTimestampPartitionKey, } from './queries'; +import { RequestHandlerContext, KibanaRequest } from '../../../../../../../src/core/server'; const COMPOSITE_AGGREGATION_BATCH_SIZE = 1000; export class InfraLogAnalysis { constructor( private readonly libs: { - framework: InfraBackendFrameworkAdapter; + framework: KibanaFramework; } ) {} - public getJobIds(request: InfraFrameworkRequest, sourceId: string) { + public getJobIds(request: KibanaRequest, sourceId: string) { return { logEntryRate: getJobId(this.libs.framework.getSpaceId(request), sourceId, 'log-entry-rate'), }; } public async getLogEntryRateBuckets( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, startTime: number, endTime: number, - bucketDuration: number + bucketDuration: number, + request: KibanaRequest ) { const logRateJobId = this.getJobIds(request, sourceId).logEntryRate; - let mlModelPlotBuckets: LogRateModelPlotBucket[] = []; let afterLatestBatchKey: CompositeTimestampPartitionKey | undefined; while (true) { const mlModelPlotResponse = await this.libs.framework.callWithRequest( - request, + requestContext, 'search', createLogEntryRateQuery( logRateJobId, diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts index 741293f61056e..59a4e8911a94d 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts @@ -5,6 +5,7 @@ */ import { idx } from '@kbn/elastic-idx'; +import { RequestHandlerContext } from 'src/core/server'; import { InfraSnapshotGroupbyInput, InfraSnapshotMetricInput, @@ -13,11 +14,8 @@ import { InfraNodeType, InfraSourceConfiguration, } from '../../graphql/types'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraDatabaseSearchResponse, -} from '../adapters/framework'; +import { InfraDatabaseSearchResponse } from '../adapters/framework'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraSources } from '../sources'; import { JsonObject } from '../../../common/typed_json'; @@ -49,20 +47,18 @@ export interface InfraSnapshotRequestOptions { } export class InfraSnapshot { - constructor( - private readonly libs: { sources: InfraSources; framework: InfraBackendFrameworkAdapter } - ) {} + constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {} public async getNodes( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions ): Promise { // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch // in order to page through the results of their respective composite aggregations. // Both chains of requests are supposed to run in parallel, and their results be merged // when they have both been completed. - const groupedNodesPromise = requestGroupedNodes(request, options, this.libs.framework); - const nodeMetricsPromise = requestNodeMetrics(request, options, this.libs.framework); + const groupedNodesPromise = requestGroupedNodes(requestContext, options, this.libs.framework); + const nodeMetricsPromise = requestNodeMetrics(requestContext, options, this.libs.framework); const groupedNodeBuckets = await groupedNodesPromise; const nodeMetricBuckets = await nodeMetricsPromise; @@ -79,9 +75,9 @@ const handleAfterKey = createAfterKeyHandler('body.aggregations.nodes.composite. ); const requestGroupedNodes = async ( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions, - framework: InfraBackendFrameworkAdapter + framework: KibanaFramework ): Promise => { const query = { allowNoIndices: true, @@ -130,13 +126,13 @@ const requestGroupedNodes = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeGroupByBucket - >(framework, request, query, bucketSelector, handleAfterKey); + >(framework, requestContext, query, bucketSelector, handleAfterKey); }; const requestNodeMetrics = async ( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, options: InfraSnapshotRequestOptions, - framework: InfraBackendFrameworkAdapter + framework: KibanaFramework ): Promise => { const index = options.metric.type === 'logRate' @@ -191,7 +187,7 @@ const requestNodeMetrics = async ( return await getAllCompositeData< InfraSnapshotAggregationResponse, InfraSnapshotNodeMetricsBucket - >(framework, request, query, bucketSelector, handleAfterKey); + >(framework, requestContext, query, bucketSelector, handleAfterKey); }; // buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[] diff --git a/x-pack/legacy/plugins/infra/server/lib/source_status.ts b/x-pack/legacy/plugins/infra/server/lib/source_status.ts index f9f37b5aa9e5a..1f0845b6b223f 100644 --- a/x-pack/legacy/plugins/infra/server/lib/source_status.ts +++ b/x-pack/legacy/plugins/infra/server/lib/source_status.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraFrameworkRequest } from './adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; import { InfraSources } from './sources'; export class InfraSourceStatus { @@ -14,58 +14,85 @@ export class InfraSourceStatus { ) {} public async getLogIndexNames( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string ): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const indexNames = await this.adapter.getIndexNames( - request, + requestContext, sourceConfiguration.configuration.logAlias ); return indexNames; } public async getMetricIndexNames( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string ): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const indexNames = await this.adapter.getIndexNames( - request, + requestContext, sourceConfiguration.configuration.metricAlias ); return indexNames; } - public async hasLogAlias(request: InfraFrameworkRequest, sourceId: string): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + public async hasLogAlias( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasAlias = await this.adapter.hasAlias( - request, + requestContext, sourceConfiguration.configuration.logAlias ); return hasAlias; } - public async hasMetricAlias(request: InfraFrameworkRequest, sourceId: string): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + public async hasMetricAlias( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasAlias = await this.adapter.hasAlias( - request, + requestContext, sourceConfiguration.configuration.metricAlias ); return hasAlias; } - public async hasLogIndices(request: InfraFrameworkRequest, sourceId: string): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + public async hasLogIndices( + requestContext: RequestHandlerContext, + sourceId: string + ): Promise { + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasIndices = await this.adapter.hasIndices( - request, + requestContext, sourceConfiguration.configuration.logAlias ); return hasIndices; } public async hasMetricIndices( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string ): Promise { - const sourceConfiguration = await this.libs.sources.getSourceConfiguration(request, sourceId); + const sourceConfiguration = await this.libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const hasIndices = await this.adapter.hasIndices( - request, + requestContext, sourceConfiguration.configuration.metricAlias ); return hasIndices; @@ -73,7 +100,7 @@ export class InfraSourceStatus { } export interface InfraSourceStatusAdapter { - getIndexNames(request: InfraFrameworkRequest, aliasName: string): Promise; - hasAlias(request: InfraFrameworkRequest, aliasName: string): Promise; - hasIndices(request: InfraFrameworkRequest, indexNames: string): Promise; + getIndexNames(requestContext: RequestHandlerContext, aliasName: string): Promise; + hasAlias(requestContext: RequestHandlerContext, aliasName: string): Promise; + hasIndices(requestContext: RequestHandlerContext, indexNames: string): Promise; } diff --git a/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts index 2374a83a642df..4a83ca730ff83 100644 --- a/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/legacy/plugins/infra/server/lib/sources/sources.test.ts @@ -3,34 +3,31 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { InfraInmemoryConfigurationAdapter } from '../adapters/configuration/inmemory_configuration_adapter'; import { InfraSources } from './sources'; describe('the InfraSources lib', () => { describe('getSourceConfiguration method', () => { test('returns a source configuration if it exists', async () => { const sourcesLib = new InfraSources({ - configuration: createMockStaticConfiguration({}), - savedObjects: createMockSavedObjectsService({ - id: 'TEST_ID', - version: 'foo', - updated_at: '2000-01-01T00:00:00.000Z', - attributes: { - metricAlias: 'METRIC_ALIAS', - logAlias: 'LOG_ALIAS', - fields: { - container: 'CONTAINER', - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, - }, - }), + config: createMockStaticConfiguration({}), }); - const request: any = Symbol(); + const request: any = createRequestContext({ + id: 'TEST_ID', + version: 'foo', + updated_at: '2000-01-01T00:00:00.000Z', + attributes: { + metricAlias: 'METRIC_ALIAS', + logAlias: 'LOG_ALIAS', + fields: { + container: 'CONTAINER', + host: 'HOST', + pod: 'POD', + tiebreaker: 'TIEBREAKER', + timestamp: 'TIMESTAMP', + }, + }, + }); expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', @@ -52,7 +49,7 @@ describe('the InfraSources lib', () => { test('adds missing attributes from the static configuration to a source configuration', async () => { const sourcesLib = new InfraSources({ - configuration: createMockStaticConfiguration({ + config: createMockStaticConfiguration({ default: { metricAlias: 'METRIC_ALIAS', logAlias: 'LOG_ALIAS', @@ -64,19 +61,18 @@ describe('the InfraSources lib', () => { }, }, }), - savedObjects: createMockSavedObjectsService({ - id: 'TEST_ID', - version: 'foo', - updated_at: '2000-01-01T00:00:00.000Z', - attributes: { - fields: { - container: 'CONTAINER', - }, - }, - }), }); - const request: any = Symbol(); + const request: any = createRequestContext({ + id: 'TEST_ID', + version: 'foo', + updated_at: '2000-01-01T00:00:00.000Z', + attributes: { + fields: { + container: 'CONTAINER', + }, + }, + }); expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', @@ -98,16 +94,15 @@ describe('the InfraSources lib', () => { test('adds missing attributes from the default configuration to a source configuration', async () => { const sourcesLib = new InfraSources({ - configuration: createMockStaticConfiguration({}), - savedObjects: createMockSavedObjectsService({ - id: 'TEST_ID', - version: 'foo', - updated_at: '2000-01-01T00:00:00.000Z', - attributes: {}, - }), + config: createMockStaticConfiguration({}), }); - const request: any = Symbol(); + const request: any = createRequestContext({ + id: 'TEST_ID', + version: 'foo', + updated_at: '2000-01-01T00:00:00.000Z', + attributes: {}, + }); expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ id: 'TEST_ID', @@ -129,29 +124,30 @@ describe('the InfraSources lib', () => { }); }); -const createMockStaticConfiguration = (sources: any) => - new InfraInmemoryConfigurationAdapter({ - enabled: true, - query: { - partitionSize: 1, - partitionFactor: 1, - }, - sources, - }); - -const createMockSavedObjectsService = (savedObject?: any) => ({ - getScopedSavedObjectsClient() { - return { - async get() { - return savedObject; - }, - } as any; +const createMockStaticConfiguration = (sources: any) => ({ + enabled: true, + query: { + partitionSize: 1, + partitionFactor: 1, }, - SavedObjectsClient: { - errors: { - isNotFoundError() { - return typeof savedObject === 'undefined'; + sources, +}); + +const createRequestContext = (savedObject?: any) => { + return { + core: { + savedObjects: { + client: { + async get() { + return savedObject; + }, + errors: { + isNotFoundError() { + return typeof savedObject === 'undefined'; + }, + }, + }, }, }, - }, -}); + }; +}; diff --git a/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts b/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts index 951556a0fe642..2b38d81e4a8d5 100644 --- a/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/legacy/plugins/infra/server/lib/sources/sources.ts @@ -6,14 +6,10 @@ import * as runtimeTypes from 'io-ts'; import { failure } from 'io-ts/lib/PathReporter'; -import { Legacy } from 'kibana'; - import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; -import { Pick3 } from '../../../common/utility_types'; -import { InfraConfigurationAdapter } from '../adapters/configuration'; -import { InfraFrameworkRequest, internalInfraFrameworkRequest } from '../adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; @@ -25,19 +21,21 @@ import { SourceConfigurationSavedObjectRuntimeType, StaticSourceConfigurationRuntimeType, } from './types'; +import { InfraConfig } from '../../../../../../plugins/infra/server'; + +interface Libs { + config: InfraConfig; +} export class InfraSources { private internalSourceConfigurations: Map = new Map(); + private readonly libs: Libs; - constructor( - private readonly libs: { - configuration: InfraConfigurationAdapter; - savedObjects: Pick & - Pick3; - } - ) {} + constructor(libs: Libs) { + this.libs = libs; + } - public async getSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { + public async getSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); const savedSourceConfiguration = await this.getInternalSourceConfiguration(sourceId) @@ -53,7 +51,7 @@ export class InfraSources { })) .catch(err => err instanceof NotFoundError - ? this.getSavedSourceConfiguration(request, sourceId).then(result => ({ + ? this.getSavedSourceConfiguration(requestContext, sourceId).then(result => ({ ...result, configuration: mergeSourceConfiguration( staticDefaultSourceConfiguration, @@ -63,7 +61,7 @@ export class InfraSources { : Promise.reject(err) ) .catch(err => - this.libs.savedObjects.SavedObjectsClient.errors.isNotFoundError(err) + requestContext.core.savedObjects.client.errors.isNotFoundError(err) ? Promise.resolve({ id: sourceId, version: undefined, @@ -77,10 +75,10 @@ export class InfraSources { return savedSourceConfiguration; } - public async getAllSourceConfigurations(request: InfraFrameworkRequest) { + public async getAllSourceConfigurations(requestContext: RequestHandlerContext) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(request); + const savedSourceConfigurations = await this.getAllSavedSourceConfigurations(requestContext); return savedSourceConfigurations.map(savedSourceConfiguration => ({ ...savedSourceConfiguration, @@ -92,7 +90,7 @@ export class InfraSources { } public async createSourceConfiguration( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, source: InfraSavedSourceConfiguration ) { @@ -104,13 +102,11 @@ export class InfraSources { ); const createdSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalInfraFrameworkRequest]) - .create( - infraSourceConfigurationSavedObjectType, - pickSavedSourceConfiguration(newSourceConfiguration) as any, - { id: sourceId } - ) + await requestContext.core.savedObjects.client.create( + infraSourceConfigurationSavedObjectType, + pickSavedSourceConfiguration(newSourceConfiguration) as any, + { id: sourceId } + ) ); return { @@ -122,20 +118,21 @@ export class InfraSources { }; } - public async deleteSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalInfraFrameworkRequest]) - .delete(infraSourceConfigurationSavedObjectType, sourceId); + public async deleteSourceConfiguration(requestContext: RequestHandlerContext, sourceId: string) { + await requestContext.core.savedObjects.client.delete( + infraSourceConfigurationSavedObjectType, + sourceId + ); } public async updateSourceConfiguration( - request: InfraFrameworkRequest, + requestContext: RequestHandlerContext, sourceId: string, sourceProperties: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const { configuration, version } = await this.getSourceConfiguration(request, sourceId); + const { configuration, version } = await this.getSourceConfiguration(requestContext, sourceId); const updatedSourceConfigurationAttributes = mergeSourceConfiguration( configuration, @@ -143,16 +140,14 @@ export class InfraSources { ); const updatedSourceConfiguration = convertSavedObjectToSavedSourceConfiguration( - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalInfraFrameworkRequest]) - .update( - infraSourceConfigurationSavedObjectType, - sourceId, - pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, - { - version, - } - ) + await requestContext.core.savedObjects.client.update( + infraSourceConfigurationSavedObjectType, + sourceId, + pickSavedSourceConfiguration(updatedSourceConfigurationAttributes) as any, + { + version, + } + ) ); return { @@ -184,7 +179,6 @@ export class InfraSources { } private async getStaticDefaultSourceConfiguration() { - const staticConfiguration = await this.libs.configuration.get(); const staticSourceConfiguration = pipe( runtimeTypes .type({ @@ -192,7 +186,7 @@ export class InfraSources { default: StaticSourceConfigurationRuntimeType, }), }) - .decode(staticConfiguration), + .decode(this.libs.config), map(({ sources: { default: defaultConfiguration } }) => defaultConfiguration), fold(constant({}), identity) ); @@ -200,12 +194,11 @@ export class InfraSources { return mergeSourceConfiguration(defaultSourceConfiguration, staticSourceConfiguration); } - private async getSavedSourceConfiguration(request: InfraFrameworkRequest, sourceId: string) { - const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient( - request[internalInfraFrameworkRequest] - ); - - const savedObject = await savedObjectsClient.get( + private async getSavedSourceConfiguration( + requestContext: RequestHandlerContext, + sourceId: string + ) { + const savedObject = await requestContext.core.savedObjects.client.get( infraSourceConfigurationSavedObjectType, sourceId ); @@ -213,12 +206,8 @@ export class InfraSources { return convertSavedObjectToSavedSourceConfiguration(savedObject); } - private async getAllSavedSourceConfigurations(request: InfraFrameworkRequest) { - const savedObjectsClient = this.libs.savedObjects.getScopedSavedObjectsClient( - request[internalInfraFrameworkRequest] - ); - - const savedObjects = await savedObjectsClient.find({ + private async getAllSavedSourceConfigurations(requestContext: RequestHandlerContext) { + const savedObjects = await requestContext.core.savedObjects.client.find({ type: infraSourceConfigurationSavedObjectType, }); diff --git a/x-pack/legacy/plugins/infra/server/new_platform_index.ts b/x-pack/legacy/plugins/infra/server/new_platform_index.ts new file mode 100644 index 0000000000000..6b759ecfe9fde --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/new_platform_index.ts @@ -0,0 +1,16 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { InfraServerPlugin } from './new_platform_plugin'; +import { config, InfraConfig } from '../../../../plugins/infra/server'; +import { InfraServerPluginDeps } from './lib/adapters/framework'; + +export { config, InfraConfig, InfraServerPluginDeps }; + +export function plugin(context: PluginInitializerContext) { + return new InfraServerPlugin(context); +} diff --git a/x-pack/legacy/plugins/infra/server/new_platform_plugin.ts b/x-pack/legacy/plugins/infra/server/new_platform_plugin.ts new file mode 100644 index 0000000000000..462a07574b2dd --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/new_platform_plugin.ts @@ -0,0 +1,107 @@ +/* + * 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 { CoreSetup, PluginInitializerContext } from 'src/core/server'; +import { Server } from 'hapi'; +import { InfraConfig } from '../../../../plugins/infra/server'; +import { initInfraServer } from './infra_server'; +import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; +import { FrameworkFieldsAdapter } from './lib/adapters/fields/framework_fields_adapter'; +import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapter'; +import { InfraKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter'; +import { KibanaMetricsAdapter } from './lib/adapters/metrics/kibana_metrics_adapter'; +import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_status'; +import { InfraFieldsDomain } from './lib/domains/fields_domain'; +import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain'; +import { InfraMetricsDomain } from './lib/domains/metrics_domain'; +import { InfraLogAnalysis } from './lib/log_analysis'; +import { InfraSnapshot } from './lib/snapshot'; +import { InfraSourceStatus } from './lib/source_status'; +import { InfraSources } from './lib/sources'; +import { InfraServerPluginDeps } from './lib/adapters/framework'; +import { METRICS_FEATURE, LOGS_FEATURE } from './features'; +import { UsageCollector } from './usage/usage_collector'; + +export interface KbnServer extends Server { + usage: any; +} + +const DEFAULT_CONFIG: InfraConfig = { + enabled: true, + query: { + partitionSize: 75, + partitionFactor: 1.2, + }, +}; + +export class InfraServerPlugin { + public config: InfraConfig = DEFAULT_CONFIG; + public libs: InfraBackendLibs | undefined; + + constructor(context: PluginInitializerContext) { + const config$ = context.config.create(); + config$.subscribe(configValue => { + this.config = { + ...DEFAULT_CONFIG, + enabled: configValue.enabled, + query: { + ...DEFAULT_CONFIG.query, + ...configValue.query, + }, + }; + }); + } + + getLibs() { + if (!this.libs) { + throw new Error('libs not set up yet'); + } + return this.libs; + } + + setup(core: CoreSetup, plugins: InfraServerPluginDeps) { + const framework = new KibanaFramework(core, this.config, plugins); + const sources = new InfraSources({ + config: this.config, + }); + const sourceStatus = new InfraSourceStatus( + new InfraElasticsearchSourceStatusAdapter(framework), + { + sources, + } + ); + const snapshot = new InfraSnapshot({ sources, framework }); + const logAnalysis = new InfraLogAnalysis({ framework }); + + // TODO: separate these out individually and do away with "domains" as a temporary group + const domainLibs: InfraDomainLibs = { + fields: new InfraFieldsDomain(new FrameworkFieldsAdapter(framework), { + sources, + }), + logEntries: new InfraLogEntriesDomain(new InfraKibanaLogEntriesAdapter(framework), { + sources, + }), + metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), + }; + + this.libs = { + configuration: this.config, + framework, + logAnalysis, + snapshot, + sources, + sourceStatus, + ...domainLibs, + }; + + plugins.features.registerFeature(METRICS_FEATURE); + plugins.features.registerFeature(LOGS_FEATURE); + + initInfraServer(this.libs); + + // Telemetry + UsageCollector.registerUsageCollector(plugins.usageCollection); + } +} diff --git a/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts b/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts index 16837298f0704..5ad79b3d17a13 100644 --- a/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts +++ b/x-pack/legacy/plugins/infra/server/routes/ip_to_hostname.ts @@ -3,18 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { boomify, notFound } from 'boom'; import { first } from 'lodash'; +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../lib/infra_types'; -import { InfraWrappableRequest } from '../lib/adapters/framework'; - -interface IpToHostRequest { - ip: string; - index_pattern: string; -} - -type IpToHostWrappedRequest = InfraWrappableRequest; export interface IpToHostResponse { host: string; @@ -28,40 +19,47 @@ interface HostDoc { }; } -const ipToHostSchema = Joi.object({ - ip: Joi.string().required(), - index_pattern: Joi.string().required(), +const ipToHostSchema = schema.object({ + ip: schema.string(), + index_pattern: schema.string(), }); export const initIpToHostName = ({ framework }: InfraBackendLibs) => { const { callWithRequest } = framework; - framework.registerRoute>({ - method: 'POST', - path: '/api/infra/ip_to_host', - options: { - validate: { payload: ipToHostSchema }, + framework.registerRoute( + { + method: 'post', + path: '/api/infra/ip_to_host', + validate: { + body: ipToHostSchema, + }, }, - handler: async req => { + async (requestContext, { body }, response) => { try { const params = { - index: req.payload.index_pattern, + index: body.index_pattern, body: { size: 1, query: { - match: { 'host.ip': req.payload.ip }, + match: { 'host.ip': body.ip }, }, _source: ['host.name'], }, }; - const response = await callWithRequest(req, 'search', params); - if (response.hits.total.value === 0) { - throw notFound('Host with matching IP address not found.'); + const { hits } = await callWithRequest(requestContext, 'search', params); + if (hits.total.value === 0) { + return response.notFound({ + body: { message: 'Host with matching IP address not found.' }, + }); } - const hostDoc = first(response.hits.hits); - return { host: hostDoc._source.host.name }; - } catch (e) { - throw boomify(e); + const hostDoc = first(hits.hits); + return response.ok({ body: { host: hostDoc._source.host.name } }); + } catch ({ statusCode = 500, message = 'Unknown error occurred' }) { + return response.customError({ + statusCode, + body: { message }, + }); } - }, - }); + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts index 0a369adb7ca29..1f64da1859b5f 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/index_patterns/validate.ts @@ -8,7 +8,7 @@ import Boom from 'boom'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; - +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { LOG_ANALYSIS_VALIDATION_INDICES_PATH, @@ -20,64 +20,75 @@ import { import { throwErrors } from '../../../../common/runtime_types'; const partitionField = 'event.dataset'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); export const initIndexPatternsValidateRoute = ({ framework }: InfraBackendLibs) => { - framework.registerRoute({ - method: 'POST', - path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, - handler: async (req, res) => { - const payload = pipe( - validationIndicesRequestPayloadRT.decode(req.payload), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { timestampField, indices } = payload.data; - const errors: ValidationIndicesError[] = []; + framework.registerRoute( + { + method: 'post', + path: LOG_ANALYSIS_VALIDATION_INDICES_PATH, + validate: { body: escapeHatch }, + }, + async (requestContext, request, response) => { + try { + const payload = pipe( + validationIndicesRequestPayloadRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - // Query each pattern individually, to map correctly the errors - await Promise.all( - indices.map(async index => { - const fieldCaps = await framework.callWithRequest(req, 'fieldCaps', { - index, - fields: `${timestampField},${partitionField}`, - }); + const { timestampField, indices } = payload.data; + const errors: ValidationIndicesError[] = []; - if (fieldCaps.indices.length === 0) { - errors.push({ - error: 'INDEX_NOT_FOUND', + // Query each pattern individually, to map correctly the errors + await Promise.all( + indices.map(async index => { + const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { index, + fields: `${timestampField},${partitionField}`, }); - return; - } - ([ - [timestampField, 'date'], - [partitionField, 'keyword'], - ] as const).forEach(([field, fieldType]) => { - const fieldMetadata = fieldCaps.fields[field]; - - if (fieldMetadata === undefined) { + if (fieldCaps.indices.length === 0) { errors.push({ - error: 'FIELD_NOT_FOUND', + error: 'INDEX_NOT_FOUND', index, - field, }); - } else { - const fieldTypes = Object.keys(fieldMetadata); + return; + } + + ([ + [timestampField, 'date'], + [partitionField, 'keyword'], + ] as const).forEach(([field, fieldType]) => { + const fieldMetadata = fieldCaps.fields[field]; - if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + if (fieldMetadata === undefined) { errors.push({ - error: `FIELD_NOT_VALID`, + error: 'FIELD_NOT_FOUND', index, field, }); - } - } - }); - }) - ); + } else { + const fieldTypes = Object.keys(fieldMetadata); - return res.response(validationIndicesResponsePayloadRT.encode({ data: { errors } })); - }, - }); + if (fieldTypes.length > 1 || fieldTypes[0] !== fieldType) { + errors.push({ + error: `FIELD_NOT_VALID`, + index, + field, + }); + } + } + }); + }) + ); + return response.ok({ + body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts index fc06ea48f4353..973080c880e6d 100644 --- a/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ b/x-pack/legacy/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts @@ -9,6 +9,7 @@ import Boom from 'boom'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../../lib/infra_types'; import { LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, @@ -19,46 +20,58 @@ import { import { throwErrors } from '../../../../common/runtime_types'; import { NoLogRateResultsIndexError } from '../../../lib/log_analysis'; +const anyObject = schema.object({}, { allowUnknowns: true }); + export const initLogAnalysisGetLogEntryRateRoute = ({ framework, logAnalysis, }: InfraBackendLibs) => { - framework.registerRoute({ - method: 'POST', - path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - handler: async (req, res) => { + framework.registerRoute( + { + method: 'post', + path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, + validate: { + // short-circuit forced @kbn/config-schema validation so we can do io-ts validation + body: anyObject, + }, + }, + async (requestContext, request, response) => { const payload = pipe( - getLogEntryRateRequestPayloadRT.decode(req.payload), + getLogEntryRateRequestPayloadRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const logEntryRateBuckets = await logAnalysis - .getLogEntryRateBuckets( - req, + try { + const logEntryRateBuckets = await logAnalysis.getLogEntryRateBuckets( + requestContext, payload.data.sourceId, payload.data.timeRange.startTime, payload.data.timeRange.endTime, - payload.data.bucketDuration - ) - .catch(err => { - if (err instanceof NoLogRateResultsIndexError) { - throw Boom.boomify(err, { statusCode: 404 }); - } + payload.data.bucketDuration, + request + ); - throw Boom.boomify(err, { statusCode: ('statusCode' in err && err.statusCode) || 500 }); + return response.ok({ + body: getLogEntryRateSuccessReponsePayloadRT.encode({ + data: { + bucketDuration: payload.data.bucketDuration, + histogramBuckets: logEntryRateBuckets, + totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets), + }, + }), }); - - return res.response( - getLogEntryRateSuccessReponsePayloadRT.encode({ - data: { - bucketDuration: payload.data.bucketDuration, - histogramBuckets: logEntryRateBuckets, - totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets), - }, - }) - ); - }, - }); + } catch (e) { + const { statusCode = 500, message = 'Unknown error occurred' } = e; + if (e instanceof NoLogRateResultsIndexError) { + return response.notFound({ body: { message } }); + } + return response.customError({ + statusCode, + body: { message }, + }); + } + } + ); }; const getTotalNumberOfLogEntries = ( diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts index 8cdb121aebf1e..a1f6311a103eb 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/index.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom, { boomify } from 'boom'; +import { schema } from '@kbn/config-schema'; +import Boom from 'boom'; import { get } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { - InfraMetadata, - InfraMetadataWrappedRequest, InfraMetadataFeature, InfraMetadataRequestRT, InfraMetadataRT, @@ -24,23 +23,33 @@ import { getCloudMetricsMetadata } from './lib/get_cloud_metric_metadata'; import { getNodeInfo } from './lib/get_node_info'; import { throwErrors } from '../../../common/runtime_types'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); + export const initMetadataRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - framework.registerRoute>({ - method: 'POST', - path: '/api/infra/metadata', - handler: async req => { + framework.registerRoute( + { + method: 'post', + path: '/api/infra/metadata', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { try { const { nodeId, nodeType, sourceId } = pipe( - InfraMetadataRequestRT.decode(req.payload), + InfraMetadataRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const { configuration } = await libs.sources.getSourceConfiguration(req, sourceId); + const { configuration } = await libs.sources.getSourceConfiguration( + requestContext, + sourceId + ); const metricsMetadata = await getMetricMetadata( framework, - req, + requestContext, configuration, nodeId, nodeType @@ -49,35 +58,35 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { nameToFeature('metrics') ); - const info = await getNodeInfo(framework, req, configuration, nodeId, nodeType); + const info = await getNodeInfo(framework, requestContext, configuration, nodeId, nodeType); const cloudInstanceId = get(info, 'cloud.instance.id'); const cloudMetricsMetadata = cloudInstanceId - ? await getCloudMetricsMetadata(framework, req, configuration, cloudInstanceId) + ? await getCloudMetricsMetadata(framework, requestContext, configuration, cloudInstanceId) : { buckets: [] }; const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( nameToFeature('metrics') ); - - const hasAPM = await hasAPMData(framework, req, configuration, nodeId, nodeType); + const hasAPM = await hasAPMData(framework, requestContext, configuration, nodeId, nodeType); const apmMetricFeatures = hasAPM ? [{ name: 'apm.transaction', source: 'apm' }] : []; const id = metricsMetadata.id; const name = metricsMetadata.name || id; - return pipe( - InfraMetadataRT.decode({ + return response.ok({ + body: InfraMetadataRT.encode({ id, name, features: [...metricFeatures, ...cloudMetricsFeatures, ...apmMetricFeatures], info, }), - fold(throwErrors(Boom.badImplementation), identity) - ); + }); } catch (error) { - throw boomify(error); + return response.internalError({ + body: error.message, + }); } - }, - }); + } + ); }; const nameToFeature = (source: string) => (name: string): InfraMetadataFeature => ({ diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts index 58b3beab42886..75ca3ae3caee2 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RequestHandlerContext } from 'src/core/server'; import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, InfraMetadataAggregationBucket, InfraMetadataAggregationResponse, } from '../../../lib/adapters/framework'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; @@ -18,8 +18,8 @@ export interface InfraCloudMetricsAdapterResponse { } export const getCloudMetricsMetadata = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, instanceId: string ): Promise => { @@ -51,7 +51,7 @@ export const getCloudMetricsMetadata = async ( { metrics?: InfraMetadataAggregationResponse; } - >(req, 'search', metricQuery); + >(requestContext, 'search', metricQuery); const buckets = response.aggregations && response.aggregations.metrics diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts index 812bc27fffc8a..3bd22062c26a0 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts @@ -5,12 +5,12 @@ */ import { get } from 'lodash'; +import { RequestHandlerContext } from 'src/core/server'; import { - InfraFrameworkRequest, InfraMetadataAggregationBucket, - InfraBackendFrameworkAdapter, InfraMetadataAggregationResponse, } from '../../../lib/adapters/framework'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { getIdFieldName } from './get_id_field_name'; import { NAME_FIELDS } from '../../../lib/constants'; @@ -22,8 +22,8 @@ export interface InfraMetricsAdapterResponse { } export const getMetricMetadata = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' @@ -69,7 +69,7 @@ export const getMetricMetadata = async ( metrics?: InfraMetadataAggregationResponse; nodeName?: InfraMetadataAggregationResponse; } - >(req, 'search', metricQuery); + >(requestContext, 'search', metricQuery); const buckets = response.aggregations && response.aggregations.metrics diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts index 5af25515a42ed..1567b6d1bd1ec 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -5,10 +5,8 @@ */ import { first } from 'lodash'; -import { - InfraFrameworkRequest, - InfraBackendFrameworkAdapter, -} from '../../../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { InfraNodeType } from '../../../graphql/types'; import { InfraMetadataInfo } from '../../../../common/http_api/metadata_api'; @@ -17,8 +15,8 @@ import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; import { getIdFieldName } from './get_id_field_name'; export const getNodeInfo = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' @@ -31,7 +29,7 @@ export const getNodeInfo = async ( if (nodeType === InfraNodeType.pod) { const kubernetesNodeName = await getPodNodeName( framework, - req, + requestContext, sourceConfiguration, nodeId, nodeType @@ -39,7 +37,7 @@ export const getNodeInfo = async ( if (kubernetesNodeName) { return getNodeInfo( framework, - req, + requestContext, sourceConfiguration, kubernetesNodeName, InfraNodeType.host @@ -64,7 +62,7 @@ export const getNodeInfo = async ( }, }; const response = await framework.callWithRequest<{ _source: InfraMetadataInfo }, {}>( - req, + requestContext, 'search', params ); diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts index 893707a4660ee..47ffc7f83b6bc 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -5,16 +5,14 @@ */ import { first, get } from 'lodash'; -import { - InfraFrameworkRequest, - InfraBackendFrameworkAdapter, -} from '../../../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { getIdFieldName } from './get_id_field_name'; export const getPodNodeName = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' @@ -40,7 +38,7 @@ export const getPodNodeName = async ( const response = await framework.callWithRequest< { _source: { kubernetes: { node: { name: string } } } }, {} - >(req, 'search', params); + >(requestContext, 'search', params); const firstHit = first(response.hits.hits); if (firstHit) { return get(firstHit, '_source.kubernetes.node.name'); diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts index 3193cf83978b0..ab242804173c0 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts @@ -4,22 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - InfraFrameworkRequest, - InfraBackendFrameworkAdapter, -} from '../../../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; + +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { getIdFieldName } from './get_id_field_name'; export const hasAPMData = async ( - framework: InfraBackendFrameworkAdapter, - req: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, nodeType: 'host' | 'pod' | 'container' ) => { - const config = framework.config(req); - const apmIndex = config.get('apm_oss.transactionIndices') || 'apm-*'; + const apmIndices = await framework.plugins.apm.getApmIndices( + requestContext.core.savedObjects.client + ); + const apmIndex = apmIndices['apm_oss.transactionIndices'] || 'apm-*'; + // There is a bug in APM ECS data where host.name is not set. // This will fixed with: https://github.com/elastic/apm-server/issues/2502 const nodeFieldName = @@ -48,6 +50,6 @@ export const hasAPMData = async ( }, }, }; - const response = await framework.callWithRequest<{}, {}>(req, 'search', params); + const response = await framework.callWithRequest<{}, {}>(requestContext, 'search', params); return response.hits.total.value !== 0; }; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts index 6b724f6ac60fd..0c69034c66940 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/index.ts @@ -4,42 +4,50 @@ * you may not use this file except in compliance with the Elastic License. */ -import { boomify } from 'boom'; +import { schema } from '@kbn/config-schema'; import { InfraBackendLibs } from '../../lib/infra_types'; import { getGroupings } from './lib/get_groupings'; import { populateSeriesWithTSVBData } from './lib/populate_series_with_tsvb_data'; -import { metricsExplorerSchema } from './schema'; -import { MetricsExplorerResponse, MetricsExplorerWrappedRequest } from './types'; +import { MetricsExplorerRequestBody } from './types'; +// import { metricsExplorerSchema } from './schema'; +// import { MetricsExplorerResponse, MetricsExplorerRequestBody } from './types'; + +// NP_TODO: need to replace all of this with real types or io-ts or something? +const escapeHatch = schema.object({}, { allowUnknowns: true }); export const initMetricExplorerRoute = (libs: InfraBackendLibs) => { const { framework } = libs; const { callWithRequest } = framework; - framework.registerRoute>({ - method: 'POST', - path: '/api/infra/metrics_explorer', - options: { + framework.registerRoute( + { + method: 'post', + path: '/api/infra/metrics_explorer', validate: { - payload: metricsExplorerSchema, + body: escapeHatch, }, }, - handler: async req => { + async (requestContext, request, response) => { try { const search = (searchOptions: object) => - callWithRequest<{}, Aggregation>(req, 'search', searchOptions); - const options = req.payload; + callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions); + const options = request.body as MetricsExplorerRequestBody; // Need to remove this casting and swap in config-schema demands :( // First we get the groupings from a composite aggregation - const response = await getGroupings(search, options); + const groupings = await getGroupings(search, options); // Then we take the results and fill in the data from TSVB with the // user's custom metrics const seriesWithMetrics = await Promise.all( - response.series.map(populateSeriesWithTSVBData(req, options, framework)) + groupings.series.map( + populateSeriesWithTSVBData(request, options, framework, requestContext) + ) ); - return { ...response, series: seriesWithMetrics }; + return response.ok({ body: { ...groupings, series: seriesWithMetrics } }); } catch (error) { - throw boomify(error); + return response.internalError({ + body: error.message, + }); } - }, - }); + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts index 6b7f85f7e5952..64b9fba0e7aa2 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/create_metrics_model.ts @@ -5,10 +5,10 @@ */ import { InfraMetricModelMetricType } from '../../../lib/adapters/metrics'; -import { MetricsExplorerAggregation, MetricsExplorerRequest } from '../types'; +import { MetricsExplorerAggregation, MetricsExplorerRequestBody } from '../types'; import { InfraMetric } from '../../../graphql/types'; import { TSVBMetricModel } from '../../../../common/inventory_models/types'; -export const createMetricModel = (options: MetricsExplorerRequest): TSVBMetricModel => { +export const createMetricModel = (options: MetricsExplorerRequestBody): TSVBMetricModel => { return { id: InfraMetric.custom, requires: [], diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts index 994de72f8029a..7111d3e7f8ca4 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts @@ -6,7 +6,7 @@ import { isObject, set } from 'lodash'; import { InfraDatabaseSearchResponse } from '../../../lib/adapters/framework'; -import { MetricsExplorerRequest, MetricsExplorerResponse } from '../types'; +import { MetricsExplorerRequestBody, MetricsExplorerResponse } from '../types'; interface GroupingAggregation { groupingsCount: { @@ -27,7 +27,7 @@ const EMPTY_RESPONSE = { export const getGroupings = async ( search: (options: object) => Promise>, - options: MetricsExplorerRequest + options: MetricsExplorerRequestBody ): Promise => { if (!options.groupBy) { return EMPTY_RESPONSE; diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts index 80ccad9567a0f..1a0edb1053730 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/lib/populate_series_with_tsvb_data.ts @@ -5,25 +5,23 @@ */ import { union } from 'lodash'; -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, -} from '../../../lib/adapters/framework'; +import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { MetricsExplorerColumnType, - MetricsExplorerRequest, MetricsExplorerRow, MetricsExplorerSeries, - MetricsExplorerWrappedRequest, + MetricsExplorerRequestBody, } from '../types'; import { createMetricModel } from './create_metrics_model'; import { JsonObject } from '../../../../common/typed_json'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; export const populateSeriesWithTSVBData = ( - req: InfraFrameworkRequest, - options: MetricsExplorerRequest, - framework: InfraBackendFrameworkAdapter + request: KibanaRequest, + options: MetricsExplorerRequestBody, + framework: KibanaFramework, + requestContext: RequestHandlerContext ) => async (series: MetricsExplorerSeries) => { // IF there are no metrics selected then we should return an empty result. if (options.metrics.length === 0) { @@ -57,7 +55,7 @@ export const populateSeriesWithTSVBData = ( const model = createMetricModel(options); const calculatedInterval = await calculateMetricInterval( framework, - req, + requestContext, { indexPattern: options.indexPattern, timestampField: options.timerange.field, @@ -78,7 +76,13 @@ export const populateSeriesWithTSVBData = ( } // Get TSVB results using the model, timerange and filters - const tsvbResults = await framework.makeTSVBRequest(req, model, timerange, filters); + const tsvbResults = await framework.makeTSVBRequest( + request, + model, + timerange, + filters, + requestContext + ); // If there is no data `custom` will not exist. if (!tsvbResults.custom) { diff --git a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts index b29c41fcbff18..a43e3adbdd184 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metrics_explorer/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraWrappableRequest } from '../../lib/adapters/framework'; - export interface InfraTimerange { field: string; from: number; @@ -27,7 +25,7 @@ export interface MetricsExplorerMetric { field?: string | undefined; } -export interface MetricsExplorerRequest { +export interface MetricsExplorerRequestBody { timerange: InfraTimerange; indexPattern: string; metrics: MetricsExplorerMetric[]; @@ -37,8 +35,6 @@ export interface MetricsExplorerRequest { filterQuery?: string; } -export type MetricsExplorerWrappedRequest = InfraWrappableRequest; - export interface MetricsExplorerPageInfo { total: number; afterKey?: string | null; diff --git a/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts b/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts index a4bc84433a4c1..a9419cd27e684 100644 --- a/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/node_details/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from 'boom'; -import { boomify } from 'boom'; +import { schema } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; @@ -13,27 +13,34 @@ import { UsageCollector } from '../../usage/usage_collector'; import { InfraMetricsRequestOptions } from '../../lib/adapters/metrics'; import { InfraNodeType, InfraMetric } from '../../graphql/types'; import { - NodeDetailsWrappedRequest, NodeDetailsRequestRT, - NodeDetailsMetricDataResponse, + NodeDetailsMetricDataResponseRT, } from '../../../common/http_api/node_details_api'; import { throwErrors } from '../../../common/runtime_types'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); + export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - framework.registerRoute>({ - method: 'POST', - path: '/api/metrics/node_details', - handler: async req => { - const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( - NodeDetailsRequestRT.decode(req.payload), - fold(throwErrors(Boom.badRequest), identity) - ); + framework.registerRoute( + { + method: 'post', + path: '/api/metrics/node_details', + validate: { + body: escapeHatch, + }, + }, + async (requestContext, request, response) => { try { - const source = await libs.sources.getSourceConfiguration(req, sourceId); + const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( + NodeDetailsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); UsageCollector.countNode(nodeType); + const options: InfraMetricsRequestOptions = { nodeIds: { nodeId, @@ -44,13 +51,16 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { metrics: metrics as InfraMetric[], timerange, }; - - return { - metrics: await libs.metrics.getMetrics(req, options), - }; - } catch (e) { - throw boomify(e); + return response.ok({ + body: NodeDetailsMetricDataResponseRT.encode({ + metrics: await libs.metrics.getMetrics(requestContext, options, request), + }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); } - }, - }); + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts index 61d2fccf00101..013a261d24831 100644 --- a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from 'boom'; +import { schema } from '@kbn/config-schema'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; @@ -12,37 +13,50 @@ import { InfraSnapshotRequestOptions } from '../../lib/snapshot'; import { UsageCollector } from '../../usage/usage_collector'; import { parseFilterQuery } from '../../utils/serialized_query'; import { InfraNodeType, InfraSnapshotMetricInput } from '../../../public/graphql/types'; -import { - SnapshotRequestRT, - SnapshotWrappedRequest, - SnapshotNodeResponse, -} from '../../../common/http_api/snapshot_api'; +import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; +const escapeHatch = schema.object({}, { allowUnknowns: true }); + export const initSnapshotRoute = (libs: InfraBackendLibs) => { const { framework } = libs; - framework.registerRoute>({ - method: 'POST', - path: '/api/metrics/snapshot', - handler: async req => { - const { filterQuery, nodeType, groupBy, sourceId, metric, timerange } = pipe( - SnapshotRequestRT.decode(req.payload), - fold(throwErrors(Boom.badRequest), identity) - ); - const source = await libs.sources.getSourceConfiguration(req, sourceId); - UsageCollector.countNode(nodeType); - const options: InfraSnapshotRequestOptions = { - filterQuery: parseFilterQuery(filterQuery), - // TODO: Use common infra metric and replace graphql type - nodeType: nodeType as InfraNodeType, - groupBy, - sourceConfiguration: source.configuration, - // TODO: Use common infra metric and replace graphql type - metric: metric as InfraSnapshotMetricInput, - timerange, - }; - return { nodes: await libs.snapshot.getNodes(req, options) }; + framework.registerRoute( + { + method: 'post', + path: '/api/metrics/snapshot', + validate: { + body: escapeHatch, + }, }, - }); + async (requestContext, request, response) => { + try { + const { filterQuery, nodeType, groupBy, sourceId, metric, timerange } = pipe( + SnapshotRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + UsageCollector.countNode(nodeType); + const options: InfraSnapshotRequestOptions = { + filterQuery: parseFilterQuery(filterQuery), + // TODO: Use common infra metric and replace graphql type + nodeType: nodeType as InfraNodeType, + groupBy, + sourceConfiguration: source.configuration, + // TODO: Use common infra metric and replace graphql type + metric: metric as InfraSnapshotMetricInput, + timerange, + }; + return response.ok({ + body: SnapshotNodeResponseRT.encode({ + nodes: await libs.snapshot.getNodes(requestContext, options), + }), + }); + } catch (error) { + return response.internalError({ + body: error.message, + }); + } + } + ); }; diff --git a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts index 7696abd2ac250..5eb5d424cdd73 100644 --- a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraBackendFrameworkAdapter, InfraFrameworkRequest } from '../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; interface Options { indexPattern: string; @@ -20,8 +21,8 @@ interface Options { * This is useful for visualizing metric modules like s3 that only send metrics once per day. */ export const calculateMetricInterval = async ( - framework: InfraBackendFrameworkAdapter, - request: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, options: Options, modules: string[] ) => { @@ -64,7 +65,11 @@ export const calculateMetricInterval = async ( }, }; - const resp = await framework.callWithRequest<{}, PeriodAggregationData>(request, 'search', query); + const resp = await framework.callWithRequest<{}, PeriodAggregationData>( + requestContext, + 'search', + query + ); // if ES doesn't return an aggregations key, something went seriously wrong. if (!resp.aggregations) { diff --git a/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts b/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts index a5729b6004dcf..c7ff1b077f685 100644 --- a/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts +++ b/x-pack/legacy/plugins/infra/server/utils/get_all_composite_data.ts @@ -4,25 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - InfraBackendFrameworkAdapter, - InfraFrameworkRequest, - InfraDatabaseSearchResponse, -} from '../lib/adapters/framework'; +import { RequestHandlerContext } from 'src/core/server'; +import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; +import { InfraDatabaseSearchResponse } from '../lib/adapters/framework'; export const getAllCompositeData = async < Aggregation = undefined, Bucket = {}, Options extends object = {} >( - framework: InfraBackendFrameworkAdapter, - request: InfraFrameworkRequest, + framework: KibanaFramework, + requestContext: RequestHandlerContext, options: Options, bucketSelector: (response: InfraDatabaseSearchResponse<{}, Aggregation>) => Bucket[], onAfterKey: (options: Options, response: InfraDatabaseSearchResponse<{}, Aggregation>) => Options, previousBuckets: Bucket[] = [] ): Promise => { - const response = await framework.callWithRequest<{}, Aggregation>(request, 'search', options); + const response = await framework.callWithRequest<{}, Aggregation>( + requestContext, + 'search', + options + ); // Nothing available, return the previous buckets. if (response.hits.total.value === 0) { @@ -45,7 +47,7 @@ export const getAllCompositeData = async < const newOptions = onAfterKey(options, response); return getAllCompositeData( framework, - request, + requestContext, newOptions, bucketSelector, onAfterKey, diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index 00945e12db51d..a1cf2ae4e8ead 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -3,7 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + SavedObjectsClientContract, +} from 'src/core/server'; import { Observable, combineLatest, AsyncSubject } from 'rxjs'; import { map } from 'rxjs/operators'; import { Server } from 'hapi'; @@ -11,6 +16,7 @@ import { once } from 'lodash'; import { Plugin as APMOSSPlugin } from '../../../../src/plugins/apm_oss/server'; import { createApmAgentConfigurationIndex } from '../../../legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index'; import { createApmApi } from '../../../legacy/plugins/apm/server/routes/create_apm_api'; +import { getApmIndices } from '../../../legacy/plugins/apm/server/lib/settings/apm_indices/get_apm_indices'; import { APMConfig, mergeConfigs, APMXPackConfig } from '.'; export interface LegacySetup { @@ -20,13 +26,18 @@ export interface LegacySetup { export interface APMPluginContract { config$: Observable; registerLegacyAPI: (__LEGACY: LegacySetup) => void; + getApmIndices: ( + savedObjectsClient: SavedObjectsClientContract + ) => ReturnType; } export class APMPlugin implements Plugin { legacySetup$: AsyncSubject; + currentConfig: APMConfig; constructor(private readonly initContext: PluginInitializerContext) { this.initContext = initContext; this.legacySetup$ = new AsyncSubject(); + this.currentConfig = {} as APMConfig; } public async setup( @@ -49,6 +60,7 @@ export class APMPlugin implements Plugin { await new Promise(resolve => { combineLatest(mergedConfig$, core.elasticsearch.dataClient$).subscribe( async ([config, dataClient]) => { + this.currentConfig = config; await createApmAgentConfigurationIndex({ esClient: dataClient, config, @@ -64,6 +76,9 @@ export class APMPlugin implements Plugin { this.legacySetup$.next(__LEGACY); this.legacySetup$.complete(); }), + getApmIndices: async (savedObjectsClient: SavedObjectsClientContract) => { + return getApmIndices({ savedObjectsClient, config: this.currentConfig }); + }, }; } diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json new file mode 100644 index 0000000000000..b0670a58ae1e8 --- /dev/null +++ b/x-pack/plugins/infra/kibana.json @@ -0,0 +1,5 @@ +{ + "id": "infra", + "version": "8.0.0", + "server": true +} diff --git a/x-pack/plugins/infra/server/index.ts b/x-pack/plugins/infra/server/index.ts new file mode 100644 index 0000000000000..b12f92c8c5a9d --- /dev/null +++ b/x-pack/plugins/infra/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext } from 'src/core/server'; +import { InfraPlugin } from './plugin'; + +export const config = { + schema: schema.object({ + enabled: schema.maybe(schema.boolean()), + query: schema.object({ + partitionSize: schema.maybe(schema.number()), + partitionFactor: schema.maybe(schema.number()), + }), + }), +}; + +export const plugin = (initContext: PluginInitializerContext) => new InfraPlugin(initContext); + +export type InfraConfig = TypeOf; +export { InfraSetup } from './plugin'; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts new file mode 100644 index 0000000000000..0c763313fb973 --- /dev/null +++ b/x-pack/plugins/infra/server/plugin.ts @@ -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 { Plugin, PluginInitializerContext } from 'src/core/server'; + +export class InfraPlugin implements Plugin { + private readonly initContext: PluginInitializerContext; + + constructor(initContext: PluginInitializerContext) { + this.initContext = initContext; + } + + public setup() { + return { + __legacy: { + config: this.initContext.config, + }, + }; + } + + public start() {} + public stop() {} +} + +export interface InfraSetup { + /** @deprecated */ + __legacy: { + config: PluginInitializerContext['config']; + }; +} diff --git a/x-pack/test/api_integration/apis/infra/feature_controls.ts b/x-pack/test/api_integration/apis/infra/feature_controls.ts index 24d378d9b9a77..6556c309f31c5 100644 --- a/x-pack/test/api_integration/apis/infra/feature_controls.ts +++ b/x-pack/test/api_integration/apis/infra/feature_controls.ts @@ -19,7 +19,6 @@ const introspectionQuery = gql` `; export default function({ getService }: FtrProviderContext) { - const supertest = getService('supertestWithoutAuth'); const security = getService('security'); const spaces = getService('spaces'); const clientFactory = getService('infraOpsGraphQLClientFactory'); @@ -37,18 +36,6 @@ export default function({ getService }: FtrProviderContext) { expect(result.response.data).to.be.an('object'); }; - const expectGraphIQL404 = (result: any) => { - expect(result.error).to.be(undefined); - expect(result.response).not.to.be(undefined); - expect(result.response).to.have.property('statusCode', 404); - }; - - const expectGraphIQLResponse = (result: any) => { - expect(result.error).to.be(undefined); - expect(result.response).not.to.be(undefined); - expect(result.response).to.have.property('statusCode', 200); - }; - const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => { const queryOptions = { query: introspectionQuery, @@ -70,16 +57,6 @@ export default function({ getService }: FtrProviderContext) { }; }; - const executeGraphIQLRequest = async (username: string, password: string, spaceId?: string) => { - const basePath = spaceId ? `/s/${spaceId}` : ''; - - return supertest - .get(`${basePath}/api/infra/graphql/graphiql`) - .auth(username, password) - .then((response: any) => ({ error: undefined, response })) - .catch((error: any) => ({ error, response: undefined })); - }; - describe('feature controls', () => { it(`APIs can't be accessed by user with logstash-* "read" privileges`, async () => { const username = 'logstash_read'; @@ -105,9 +82,6 @@ export default function({ getService }: FtrProviderContext) { const graphQLResult = await executeGraphQLQuery(username, password); expectGraphQL404(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password); - expectGraphIQL404(graphQLIResult); } finally { await security.role.delete(roleName); await security.user.delete(username); @@ -144,9 +118,6 @@ export default function({ getService }: FtrProviderContext) { const graphQLResult = await executeGraphQLQuery(username, password); expectGraphQLResponse(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password); - expectGraphIQLResponse(graphQLIResult); } finally { await security.role.delete(roleName); await security.user.delete(username); @@ -186,9 +157,6 @@ export default function({ getService }: FtrProviderContext) { const graphQLResult = await executeGraphQLQuery(username, password); expectGraphQL404(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password); - expectGraphIQL404(graphQLIResult); } finally { await security.role.delete(roleName); await security.user.delete(username); @@ -268,25 +236,16 @@ export default function({ getService }: FtrProviderContext) { it('user_1 can access APIs in space_1', async () => { const graphQLResult = await executeGraphQLQuery(username, password, space1Id); expectGraphQLResponse(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password, space1Id); - expectGraphIQLResponse(graphQLIResult); }); it(`user_1 can access APIs in space_2`, async () => { const graphQLResult = await executeGraphQLQuery(username, password, space2Id); expectGraphQLResponse(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password, space2Id); - expectGraphIQLResponse(graphQLIResult); }); it(`user_1 can't access APIs in space_3`, async () => { const graphQLResult = await executeGraphQLQuery(username, password, space3Id); expectGraphQL404(graphQLResult); - - const graphQLIResult = await executeGraphIQLRequest(username, password, space3Id); - expectGraphIQL404(graphQLIResult); }); }); }); diff --git a/x-pack/test/typings/rison_node.d.ts b/x-pack/test/typings/rison_node.d.ts deleted file mode 100644 index ec8e5c1f407ad..0000000000000 --- a/x-pack/test/typings/rison_node.d.ts +++ /dev/null @@ -1,26 +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. - */ - -declare module 'rison-node' { - export type RisonValue = null | boolean | number | string | RisonObject | RisonArray; - - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface RisonArray extends Array {} - - export interface RisonObject { - [key: string]: RisonValue; - } - - export const decode: (input: string) => RisonValue; - - // eslint-disable-next-line @typescript-eslint/camelcase - export const decode_object: (input: string) => RisonObject; - - export const encode: (input: Input) => string; - - // eslint-disable-next-line @typescript-eslint/camelcase - export const encode_object: (input: Input) => string; -} From e80611483ff42344d61cacac97a6449347b72320 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Fri, 6 Dec 2019 10:53:06 -0800 Subject: [PATCH 06/26] State containers (#52384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add state containers * docs: ✏️ add state container demos * docs: ✏️ refrech state container docs * chore: 🤖 install default comparator * chore: 🤖 remove old state container implementation * feat: 🎸 add selectors * chore: 🤖 move Ensure tyep to type utils * fix: 🐛 fix useSelector() types and demo CLI command * test: 💍 add tests for state container demos * feat: 🎸 add ReacursiveReadonly to kbn-utility-types * feat: 🎸 shallow freeze state when not in production * test: 💍 fix Jest tests * refactor: 💡 remove .state and use BehaviourSubject --- package.json | 2 + packages/kbn-utility-types/README.md | 8 +- packages/kbn-utility-types/index.ts | 16 + src/plugins/kibana_utils/README.md | 2 +- src/plugins/kibana_utils/demos/demos.test.ts | 36 +++ .../state_containers/counter.ts} | 28 +- .../demos/state_containers/todomvc.ts | 69 ++++ .../docs/state_containers/README.md | 50 +++ .../{store => state_containers}/creation.md | 17 +- .../no_react.md} | 2 +- .../docs/state_containers/react.md | 41 +++ .../docs/state_containers/react/connect.md | 22 ++ .../docs/state_containers/react/context.md | 24 ++ .../state_containers/react/use_container.md | 10 + .../state_containers/react/use_selector.md | 20 ++ .../docs/state_containers/react/use_state.md | 11 + .../state_containers/react/use_transitions.md | 17 + .../docs/state_containers/redux.md | 40 +++ .../docs/state_containers/transitions.md | 61 ++++ src/plugins/kibana_utils/docs/store/README.md | 9 - .../kibana_utils/docs/store/mutators.md | 70 ---- src/plugins/kibana_utils/docs/store/react.md | 101 ------ src/plugins/kibana_utils/docs/store/redux.md | 19 -- src/plugins/kibana_utils/public/index.test.ts | 6 +- src/plugins/kibana_utils/public/index.ts | 9 +- .../create_state_container.test.ts | 303 ++++++++++++++++++ .../create_state_container.ts | 89 +++++ ...te_state_container_react_helpers.test.tsx} | 163 ++++++---- .../create_state_container_react_helpers.ts | 77 +++++ .../{store => state_containers}/index.ts | 5 +- .../public/state_containers/types.ts | 99 ++++++ .../public/store/create_store.test.ts | 177 ---------- .../kibana_utils/public/store/create_store.ts | 85 ----- .../public/store/observable_selector.ts | 47 --- .../kibana_utils/public/store/react.ts | 126 -------- yarn.lock | 164 +++++++++- 36 files changed, 1275 insertions(+), 750 deletions(-) create mode 100644 src/plugins/kibana_utils/demos/demos.test.ts rename src/plugins/kibana_utils/{public/store/types.ts => demos/state_containers/counter.ts} (51%) create mode 100644 src/plugins/kibana_utils/demos/state_containers/todomvc.ts create mode 100644 src/plugins/kibana_utils/docs/state_containers/README.md rename src/plugins/kibana_utils/docs/{store => state_containers}/creation.md (54%) rename src/plugins/kibana_utils/docs/{store/getters.md => state_containers/no_react.md} (83%) create mode 100644 src/plugins/kibana_utils/docs/state_containers/react.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/connect.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/context.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/use_container.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/use_selector.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/use_state.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/react/use_transitions.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/redux.md create mode 100644 src/plugins/kibana_utils/docs/state_containers/transitions.md delete mode 100644 src/plugins/kibana_utils/docs/store/README.md delete mode 100644 src/plugins/kibana_utils/docs/store/mutators.md delete mode 100644 src/plugins/kibana_utils/docs/store/react.md delete mode 100644 src/plugins/kibana_utils/docs/store/redux.md create mode 100644 src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts create mode 100644 src/plugins/kibana_utils/public/state_containers/create_state_container.ts rename src/plugins/kibana_utils/public/{store/react.test.tsx => state_containers/create_state_container_react_helpers.test.tsx} (65%) create mode 100644 src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts rename src/plugins/kibana_utils/public/{store => state_containers}/index.ts (86%) create mode 100644 src/plugins/kibana_utils/public/state_containers/types.ts delete mode 100644 src/plugins/kibana_utils/public/store/create_store.test.ts delete mode 100644 src/plugins/kibana_utils/public/store/create_store.ts delete mode 100644 src/plugins/kibana_utils/public/store/observable_selector.ts delete mode 100644 src/plugins/kibana_utils/public/store/react.ts diff --git a/package.json b/package.json index 2b157da779f63..847f09b4ab4cf 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ "encode-uri-query": "1.0.1", "execa": "^3.2.0", "expiry-js": "0.1.7", + "fast-deep-equal": "^3.1.1", "file-loader": "4.2.0", "font-awesome": "4.7.0", "getos": "^3.1.0", @@ -229,6 +230,7 @@ "react-resize-detector": "^4.2.0", "react-router-dom": "^4.3.1", "react-sizeme": "^2.3.6", + "react-use": "^13.10.2", "reactcss": "1.2.3", "redux": "4.0.0", "redux-actions": "2.2.1", diff --git a/packages/kbn-utility-types/README.md b/packages/kbn-utility-types/README.md index ff6c7c7268a15..9707ff5a1ed9c 100644 --- a/packages/kbn-utility-types/README.md +++ b/packages/kbn-utility-types/README.md @@ -18,7 +18,9 @@ type B = UnwrapPromise; // string ## Reference -- `UnwrapPromise` — Returns wrapped type of a promise. -- `UnwrapObservable` — Returns wrapped type of an observable. -- `ShallowPromise` — Same as `Promise` type, but it flat maps the wrapped type. +- `Ensure` — Makes sure `T` is of type `X`. - `ObservableLike` — Minimal interface for an object resembling an `Observable`. +- `RecursiveReadonly` — Like `Readonly`, but freezes object recursively. +- `ShallowPromise` — Same as `Promise` type, but it flat maps the wrapped type. +- `UnwrapObservable` — Returns wrapped type of an observable. +- `UnwrapPromise` — Returns wrapped type of a promise. diff --git a/packages/kbn-utility-types/index.ts b/packages/kbn-utility-types/index.ts index f17890528bfd2..495b5fb374b43 100644 --- a/packages/kbn-utility-types/index.ts +++ b/packages/kbn-utility-types/index.ts @@ -42,3 +42,19 @@ export type UnwrapObservable> = T extends Observab * Converts a type to a `Promise`, unless it is already a `Promise`. Useful when proxying the return value of a possibly async function. */ export type ShallowPromise = T extends Promise ? Promise : Promise; + +/** + * Ensures T is of type X. + */ +export type Ensure = T extends X ? T : never; + +// If we define this inside RecursiveReadonly TypeScript complains. +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface RecursiveReadonlyArray extends Array> {} +export type RecursiveReadonly = T extends (...args: any) => any + ? T + : T extends any[] + ? RecursiveReadonlyArray + : T extends object + ? Readonly<{ [K in keyof T]: RecursiveReadonly }> + : T; diff --git a/src/plugins/kibana_utils/README.md b/src/plugins/kibana_utils/README.md index 61ceea2b18385..5501505dbb7e2 100644 --- a/src/plugins/kibana_utils/README.md +++ b/src/plugins/kibana_utils/README.md @@ -2,4 +2,4 @@ Utilities for building Kibana plugins. -- [Store reactive serializable app state in state containers, `createStore`](./docs/store/README.md). +- [State containers](./docs/state_containers/README.md). diff --git a/src/plugins/kibana_utils/demos/demos.test.ts b/src/plugins/kibana_utils/demos/demos.test.ts new file mode 100644 index 0000000000000..4e792ceef117a --- /dev/null +++ b/src/plugins/kibana_utils/demos/demos.test.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { result as counterResult } from './state_containers/counter'; +import { result as todomvcResult } from './state_containers/todomvc'; + +describe('demos', () => { + describe('state containers', () => { + test('counter demo works', () => { + expect(counterResult).toBe(10); + }); + + test('TodoMVC demo works', () => { + expect(todomvcResult).toEqual([ + { id: 0, text: 'Learning state containers', completed: true }, + { id: 1, text: 'Learning transitions...', completed: true }, + ]); + }); + }); +}); diff --git a/src/plugins/kibana_utils/public/store/types.ts b/src/plugins/kibana_utils/demos/state_containers/counter.ts similarity index 51% rename from src/plugins/kibana_utils/public/store/types.ts rename to src/plugins/kibana_utils/demos/state_containers/counter.ts index 952ee07f18baf..643763cc4cee9 100644 --- a/src/plugins/kibana_utils/public/store/types.ts +++ b/src/plugins/kibana_utils/demos/state_containers/counter.ts @@ -17,26 +17,16 @@ * under the License. */ -import { Observable } from 'rxjs'; -import { Store as ReduxStore } from 'redux'; +import { createStateContainer } from '../../public/state_containers'; -export interface AppStore< - State extends {}, - StateMutators extends Mutators> = {} -> { - redux: ReduxStore; - get: () => State; - set: (state: State) => void; - state$: Observable; - createMutators: >(pureMutators: M) => Mutators; - mutators: StateMutators; -} +const container = createStateContainer(0, { + increment: (cnt: number) => (by: number) => cnt + by, + double: (cnt: number) => () => cnt * 2, +}); -export type PureMutator = (state: State) => (...args: any[]) => State; -export type Mutator> = (...args: Parameters>) => void; +container.transitions.increment(5); +container.transitions.double(); -export interface PureMutators { - [name: string]: PureMutator; -} +console.log(container.get()); // eslint-disable-line -export type Mutators> = { [K in keyof M]: Mutator }; +export const result = container.get(); diff --git a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts new file mode 100644 index 0000000000000..6d0c960e2a5b2 --- /dev/null +++ b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createStateContainer, PureTransition } from '../../public/state_containers'; + +export interface TodoItem { + text: string; + completed: boolean; + id: number; +} + +export type TodoState = TodoItem[]; + +export const defaultState: TodoState = [ + { + id: 0, + text: 'Learning state containers', + completed: false, + }, +]; + +export interface TodoActions { + add: PureTransition; + edit: PureTransition; + delete: PureTransition; + complete: PureTransition; + completeAll: PureTransition; + clearCompleted: PureTransition; +} + +export const pureTransitions: TodoActions = { + add: state => todo => [...state, todo], + edit: state => todo => state.map(item => (item.id === todo.id ? { ...item, ...todo } : item)), + delete: state => id => state.filter(item => item.id !== id), + complete: state => id => + state.map(item => (item.id === id ? { ...item, completed: true } : item)), + completeAll: state => () => state.map(item => ({ ...item, completed: true })), + clearCompleted: state => () => state.filter(({ completed }) => !completed), +}; + +const container = createStateContainer(defaultState, pureTransitions); + +container.transitions.add({ + id: 1, + text: 'Learning transitions...', + completed: false, +}); +container.transitions.complete(0); +container.transitions.complete(1); + +console.log(container.get()); // eslint-disable-line + +export const result = container.get(); diff --git a/src/plugins/kibana_utils/docs/state_containers/README.md b/src/plugins/kibana_utils/docs/state_containers/README.md new file mode 100644 index 0000000000000..3b7a8b8bd4621 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/README.md @@ -0,0 +1,50 @@ +# State containers + +State containers are Redux-store-like objects meant to help you manage state in +your services or apps. + +- State containers are strongly typed, you will get TypeScript autocompletion suggestions from + your editor when accessing state, executing transitions and using React helpers. +- State containers can be easily hooked up with your React components. +- State containers can be used without React, too. +- State containers provide you central place where to store state, instead of spreading + state around multiple RxJs observables, which you need to coordinate. With state + container you can always access the latest state snapshot synchronously. +- Unlike Redux, state containers are less verbose, see example below. + + +## Example + +```ts +import { createStateContainer } from 'src/plugins/kibana_utils'; + +const container = createStateContainer(0, { + increment: (cnt: number) => (by: number) => cnt + by, + double: (cnt: number) => () => cnt * 2, +}); + +container.transitions.increment(5); +container.transitions.double(); +console.log(container.get()); // 10 +``` + + +## Demos + +See demos [here](../../demos/state_containers/). + +You can run them with + +``` +npx -q ts-node src/plugins/kibana_utils/demos/state_containers/counter.ts +npx -q ts-node src/plugins/kibana_utils/demos/state_containers/todomvc.ts +``` + + +## Reference + +- [Creating a state container](./creation.md). +- [State transitions](./transitions.md). +- [Using with React](./react.md). +- [Using without React`](./no_react.md). +- [Parallels with Redux](./redux.md). diff --git a/src/plugins/kibana_utils/docs/store/creation.md b/src/plugins/kibana_utils/docs/state_containers/creation.md similarity index 54% rename from src/plugins/kibana_utils/docs/store/creation.md rename to src/plugins/kibana_utils/docs/state_containers/creation.md index b0184ad45eb84..66d28bbd8603f 100644 --- a/src/plugins/kibana_utils/docs/store/creation.md +++ b/src/plugins/kibana_utils/docs/state_containers/creation.md @@ -17,7 +17,7 @@ interface MyState { } ``` -Create default state of your *store*. +Create default state of your container. ```ts const defaultState: MyState = { @@ -27,17 +27,12 @@ const defaultState: MyState = { }; ``` -Create your state container, i.e *store*. +Create your a state container. ```ts -import { createStore } from 'kibana-utils'; +import { createStateContainer } from 'src/plugins/kibana_utils'; -const store = createStore(defaultState); -console.log(store.get()); -``` +const container = createStateContainer(defaultState, {}); -> ##### N.B. -> -> State must always be an object `{}`. -> -> You cannot create a store out of an array, e.g ~~`createStore([])`~~. +console.log(container.get()); +``` diff --git a/src/plugins/kibana_utils/docs/store/getters.md b/src/plugins/kibana_utils/docs/state_containers/no_react.md similarity index 83% rename from src/plugins/kibana_utils/docs/store/getters.md rename to src/plugins/kibana_utils/docs/state_containers/no_react.md index 508d0c6ebc18d..7a15483d83b44 100644 --- a/src/plugins/kibana_utils/docs/store/getters.md +++ b/src/plugins/kibana_utils/docs/state_containers/no_react.md @@ -1,4 +1,4 @@ -# Reading state +# Consuming state in non-React setting To read the current `state` of the store use `.get()` method. diff --git a/src/plugins/kibana_utils/docs/state_containers/react.md b/src/plugins/kibana_utils/docs/state_containers/react.md new file mode 100644 index 0000000000000..363fd9253d44f --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react.md @@ -0,0 +1,41 @@ +# React + +`createStateContainerReactHelpers` factory allows you to easily use state containers with React. + + +## Example + + +```ts +import { createStateContainer, createStateContainerReactHelpers } from 'src/plugins/kibana_utils'; + +const container = createStateContainer({}, {}); +export const { + Provider, + Consumer, + context, + useContainer, + useState, + useTransitions, + useSelector, + connect, +} = createStateContainerReactHelpers(); +``` + +Wrap your app with ``. + +```tsx + + + +``` + + +## Reference + +- [`useContainer()`](./react/use_container.md) +- [`useState()`](./react/use_state.md) +- [`useSelector()`](./react/use_selector.md) +- [`useTransitions()`](./react/use_transitions.md) +- [`connect()()`](./react/connect.md) +- [Context](./react/context.md) diff --git a/src/plugins/kibana_utils/docs/state_containers/react/connect.md b/src/plugins/kibana_utils/docs/state_containers/react/connect.md new file mode 100644 index 0000000000000..56b7e0fbc5673 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/connect.md @@ -0,0 +1,22 @@ +# `connect()()` higher order component + +Use `connect()()` higher-order-component to inject props from state into your component. + +```tsx +interface Props { + name: string; + punctuation: '.' | ',' | '!', +} +const Demo: React.FC = ({ name, punctuation }) => +
Hello, {name}{punctuation}
; + +const store = createStateContainer({ userName: 'John' }); +const { Provider, connect } = createStateContainerReactHelpers(store); + +const mapStateToProps = ({ userName }) => ({ name: userName }); +const DemoConnected = connect(mapStateToProps)(Demo); + + + + +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/context.md b/src/plugins/kibana_utils/docs/state_containers/react/context.md new file mode 100644 index 0000000000000..33f084fdfe9d7 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/context.md @@ -0,0 +1,24 @@ +# React context + +`createStateContainerReactHelpers` returns `` and `` components +as well as `context` React context object. + +```ts +export const { + Provider, + Consumer, + context, +} = createStateContainerReactHelpers(); +``` + +`` and `` are just regular React context components. + +```tsx + +
+ {container => +
{JSON.stringify(container.get())}
+ }
+
+
+``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/use_container.md b/src/plugins/kibana_utils/docs/state_containers/react/use_container.md new file mode 100644 index 0000000000000..5e698edb8529c --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/use_container.md @@ -0,0 +1,10 @@ +# `useContainer` hook + +`useContainer` React hook will simply return you `container` object from React context. + +```tsx +const Demo = () => { + const store = useContainer(); + return
{store.get().isDarkMode ? '🌑' : '☀️'}
; +}; +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/use_selector.md b/src/plugins/kibana_utils/docs/state_containers/react/use_selector.md new file mode 100644 index 0000000000000..2ecf772fba367 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/use_selector.md @@ -0,0 +1,20 @@ +# `useSelector()` hook + +With `useSelector` React hook you specify a selector function, which will pick specific +data from the state. *Your component will update only when that specific part of the state changes.* + +```tsx +const selector = state => state.isDarkMode; +const Demo = () => { + const isDarkMode = useSelector(selector); + return
{isDarkMode ? '🌑' : '☀️'}
; +}; +``` + +As an optional second argument for `useSelector` you can provide a `comparator` function, which +compares currently selected value with the previous and your component will re-render only if +`comparator` returns `true`. By default it uses [`fast-deep-equal`](https://github.com/epoberezkin/fast-deep-equal). + +``` +useSelector(selector, comparator?) +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/use_state.md b/src/plugins/kibana_utils/docs/state_containers/react/use_state.md new file mode 100644 index 0000000000000..5db1d46897aad --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/use_state.md @@ -0,0 +1,11 @@ +# `useState()` hook + +- `useState` hook returns you directly the state of the container. +- It also forces component to re-render every time state changes. + +```tsx +const Demo = () => { + const { isDarkMode } = useState(); + return
{isDarkMode ? '🌑' : '☀️'}
; +}; +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/react/use_transitions.md b/src/plugins/kibana_utils/docs/state_containers/react/use_transitions.md new file mode 100644 index 0000000000000..c6783bf0e0f0a --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/react/use_transitions.md @@ -0,0 +1,17 @@ +# `useTransitions` hook + +Access [state transitions](../transitions.md) by `useTransitions` React hook. + +```tsx +const Demo = () => { + const { isDarkMode } = useState(); + const { setDarkMode } = useTransitions(); + return ( + <> +
{isDarkMode ? '🌑' : '☀️'}
+ + + + ); +}; +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/redux.md b/src/plugins/kibana_utils/docs/state_containers/redux.md new file mode 100644 index 0000000000000..1a60d841a8b75 --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/redux.md @@ -0,0 +1,40 @@ +# Redux + +State containers similar to Redux stores but without the boilerplate. + +State containers expose Redux-like API: + +```js +container.getState() +container.dispatch() +container.replaceReducer() +container.subscribe() +container.addMiddleware() +``` + +State containers have a reducer and every time you execute a state transition it +actually dispatches an "action". For example, this + +```js +container.transitions.increment(25); +``` + +is equivalent to + +```js +container.dispatch({ + type: 'increment', + args: [25], +}); +``` + +Because all transitions happen through `.dispatch()` interface, you can add middleware—similar how you +would do with Redux—to monitor or intercept transitions. + +For example, you can add `redux-logger` middleware to log in console all transitions happening with your store. + +```js +import logger from 'redux-logger'; + +container.addMiddleware(logger); +``` diff --git a/src/plugins/kibana_utils/docs/state_containers/transitions.md b/src/plugins/kibana_utils/docs/state_containers/transitions.md new file mode 100644 index 0000000000000..51d52cdf3daaf --- /dev/null +++ b/src/plugins/kibana_utils/docs/state_containers/transitions.md @@ -0,0 +1,61 @@ +# State transitions + +*State transitions* describe possible state changes over time. Transitions are pure functions which +receive `state` object and other—optional—arguments and must return a new `state` object back. + +```ts +type Transition = (state: State) => (...args) => State; +``` + +Transitions must not mutate `state` object in-place, instead they must return a +shallow copy of it, e.g. `{ ...state }`. Example: + +```ts +const setUiMode: PureTransition = state => uiMode => ({ ...state, uiMode }); +``` + +You provide transitions as a second argument when you create your state container. + +```ts +import { createStateContainer } from 'src/plugins/kibana_utils'; + +const container = createStateContainer(0, { + increment: (cnt: number) => (by: number) => cnt + by, + double: (cnt: number) => () => cnt * 2, +}); +``` + +Now you can execute the transitions by calling them with only optional parameters (`state` is +provided to your transitions automatically). + +```ts +container.transitions.increment(25); +container.transitions.increment(5); +container.state; // 30 +``` + +Your transitions are bound to the container so you can treat each of them as a +standalone function for export. + +```ts +const defaultState = { + uiMode: 'light', +}; + +const container = createStateContainer(defaultState, { + setUiMode: state => uiMode => ({ ...state, uiMode }), + resetUiMode: state => () => ({ ...state, uiMode: defaultState.uiMode }), +}); + +export const { + setUiMode, + resetUiMode +} = container.transitions; +``` + +You can add TypeScript annotations for your transitions as the second generic argument +to `createStateContainer()` function. + +```ts +const container = createStateContainer(defaultState, pureTransitions); +``` diff --git a/src/plugins/kibana_utils/docs/store/README.md b/src/plugins/kibana_utils/docs/store/README.md deleted file mode 100644 index e1cb098fe04ce..0000000000000 --- a/src/plugins/kibana_utils/docs/store/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# State containers - -- State containers for holding serializable state. -- [Each plugin/app that needs runtime state will create a *store* using `store = createStore()`](./creation.md). -- [*Store* can be updated using mutators `mutators = store.createMutators({ ... })`](./mutators.md). -- [*Store* can be connected to React `{Provider, connect} = createContext(store)`](./react.md). -- [In no-React setting *store* is consumed using `store.get()` and `store.state$`](./getters.md). -- [Under-the-hood uses Redux `store.redux`](./redux.md) (but you should never need it explicitly). -- [See idea doc with samples and rationale](https://docs.google.com/document/d/18eitHkcyKSsEHUfUIqFKChc8Pp62Z4gcRxdu903hbA0/edit#heading=h.iaxc9whxifl5). diff --git a/src/plugins/kibana_utils/docs/store/mutators.md b/src/plugins/kibana_utils/docs/store/mutators.md deleted file mode 100644 index 9db1b1bb60b3c..0000000000000 --- a/src/plugins/kibana_utils/docs/store/mutators.md +++ /dev/null @@ -1,70 +0,0 @@ -# Mutators - -State *mutators* are pure functions which receive `state` object and other—optional—arguments -and must return a new `state` object back. - -```ts -type Mutator = (state: State) => (...args) => State; -``` - -Mutator must not mutate `state` object in-place, instead it should return a -shallow copy of it, e.g. `{ ...state }`. - -```ts -const setUiMode: Mutator = state => uiMode => ({ ...state, uiMode }); -``` - -You create mutators using `.createMutator(...)` method. - -```ts -const store = createStore({uiMode: 'light'}); -const mutators = store.createMutators({ - setUiMode: state => uiMode => ({ ...state, uiMode }), -}); -``` - -Now you can use your mutators by calling them with only optional parameters (`state` is -provided to your mutator automatically). - -```ts -mutators.setUiMode('dark'); -``` - -Your mutators are bound to the `store` so you can treat each of them as a -standalone function for export. - -```ts -const { setUiMode, resetUiMode } = store.createMutators({ - setUiMode: state => uiMode => ({ ...state, uiMode }), - resetUiMode: state => () => ({ ...state, uiMode: 'light' }), -}); - -export { - setUiMode, - resetUiMode, -}; -``` - -The mutators you create are also available on the `store` object. - -```ts -const store = createStore({ cnt: 0 }); -store.createMutators({ - add: state => value => ({ ...state, cnt: state.cnt + value }), -}); - -store.mutators.add(5); -store.get(); // { cnt: 5 } -``` - -You can add TypeScript annotations to your `.mutators` property of `store` object. - -```ts -const store = createStore<{ - cnt: number; -}, { - add: (value: number) => void; -}>({ - cnt: 0 -}); -``` diff --git a/src/plugins/kibana_utils/docs/store/react.md b/src/plugins/kibana_utils/docs/store/react.md deleted file mode 100644 index 68a016ed6d3ca..0000000000000 --- a/src/plugins/kibana_utils/docs/store/react.md +++ /dev/null @@ -1,101 +0,0 @@ -# React - -`createContext` factory allows you to easily use state containers with React. - -```ts -import { createStore, createContext } from 'kibana-utils'; - -const store = createStore({}); -const { - Provider, - Consumer, - connect, - context, - useStore, - useState, - useMutators, - useSelector, -} = createContext(store); -``` - -Wrap your app with ``. - -```tsx - - - -``` - -Use `connect()()` higer-order-component to inject props from state into your component. - -```tsx -interface Props { - name: string; - punctuation: '.' | ',' | '!', -} -const Demo: React.FC = ({ name, punctuation }) => -
Hello, {name}{punctuation}
; - -const store = createStore({ userName: 'John' }); -const { Provider, connect } = createContext(store); - -const mapStateToProps = ({ userName }) => ({ name: userName }); -const DemoConnected = connect(mapStateToProps)(Demo); - - - - -``` - -`useStore` React hook will fetch the `store` object from the context. - -```tsx -const Demo = () => { - const store = useStore(); - return
{store.get().isDarkMode ? '🌑' : '☀️'}
; -}; -``` - -If you want your component to always re-render when the state changes use `useState` React hook. - -```tsx -const Demo = () => { - const { isDarkMode } = useState(); - return
{isDarkMode ? '🌑' : '☀️'}
; -}; -``` - -For `useSelector` React hook you specify a selector function, which will pick specific -data from the state. *Your component will update only when that specific part of the state changes.* - -```tsx -const selector = state => state.isDarkMode; -const Demo = () => { - const isDarkMode = useSelector(selector); - return
{isDarkMode ? '🌑' : '☀️'}
; -}; -``` - -As an optional second argument for `useSelector` you can provide a `comparator` function, which -compares currently selected value with the previous and your component will re-render only if -`comparator` returns `true`. By default, it simply uses tripple equals `===` comparison. - -``` -useSelector(selector, comparator?) -``` - -Access state mutators by `useMutators` React hook. - -```tsx -const Demo = () => { - const { isDarkMode } = useState(); - const { setDarkMode } = useMutators(); - return ( - <> -
{isDarkMode ? '🌑' : '☀️'}
- - - - ); -}; -``` diff --git a/src/plugins/kibana_utils/docs/store/redux.md b/src/plugins/kibana_utils/docs/store/redux.md deleted file mode 100644 index 23be76f35b36e..0000000000000 --- a/src/plugins/kibana_utils/docs/store/redux.md +++ /dev/null @@ -1,19 +0,0 @@ -# Redux - -Internally `createStore()` uses Redux to manage the state. When you call `store.get()` -it is actually calling the Redux `.getState()` method. When you execute a mutation -it is actually dispatching a Redux action. - -You can access Redux *store* using `.redux`. - -```ts -store.redux; -``` - -But you should never need it, if you think you do, consult with Kibana App Architecture team. - -We use Redux internally for 3 main reasons: - -- We can reuse `react-redux` library to easily connect state containers to React. -- We can reuse Redux devtools. -- We can reuse battle-tested Redux library and action/reducer paradigm. diff --git a/src/plugins/kibana_utils/public/index.test.ts b/src/plugins/kibana_utils/public/index.test.ts index 0e2a4acf15f04..27c4d6c1c06e9 100644 --- a/src/plugins/kibana_utils/public/index.test.ts +++ b/src/plugins/kibana_utils/public/index.test.ts @@ -17,9 +17,9 @@ * under the License. */ -import { createStore, createContext } from '.'; +import { createStateContainer, createStateContainerReactHelpers } from '.'; test('exports store methods', () => { - expect(typeof createStore).toBe('function'); - expect(typeof createContext).toBe('function'); + expect(typeof createStateContainer).toBe('function'); + expect(typeof createStateContainerReactHelpers).toBe('function'); }); diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index c5c129eca8fd3..3f5aeebac54d8 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -19,13 +19,12 @@ export * from './core'; export * from './errors'; -export * from './store'; -export * from './parse'; -export * from './resize_checker'; -export * from './render_complete'; -export * from './store'; export * from './errors'; export * from './field_mapping'; +export * from './parse'; +export * from './render_complete'; +export * from './resize_checker'; +export * from './state_containers'; export * from './storage'; export * from './storage/hashed_item_store'; export * from './state_management/state_hash'; diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts new file mode 100644 index 0000000000000..9165181299a90 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts @@ -0,0 +1,303 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createStateContainer } from './create_state_container'; + +const create = (state: S, transitions: T = {} as T) => { + const pureTransitions = { + set: () => (newState: S) => newState, + ...transitions, + }; + const store = createStateContainer(state, pureTransitions); + return { store, mutators: store.transitions }; +}; + +test('can create store', () => { + const { store } = create({}); + expect(store).toMatchObject({ + getState: expect.any(Function), + state$: expect.any(Object), + transitions: expect.any(Object), + dispatch: expect.any(Function), + subscribe: expect.any(Function), + replaceReducer: expect.any(Function), + addMiddleware: expect.any(Function), + }); +}); + +test('can set default state', () => { + const defaultState = { + foo: 'bar', + }; + const { store } = create(defaultState); + expect(store.get()).toEqual(defaultState); + expect(store.getState()).toEqual(defaultState); +}); + +test('can set state', () => { + const defaultState = { + foo: 'bar', + }; + const newState = { + foo: 'baz', + }; + const { store, mutators } = create(defaultState); + + mutators.set(newState); + + expect(store.get()).toEqual(newState); + expect(store.getState()).toEqual(newState); +}); + +test('does not shallow merge states', () => { + const defaultState = { + foo: 'bar', + }; + const newState = { + foo2: 'baz', + }; + const { store, mutators } = create(defaultState); + + mutators.set(newState as any); + + expect(store.get()).toEqual(newState); + expect(store.getState()).toEqual(newState); +}); + +test('can subscribe and unsubscribe to state changes', () => { + const { store, mutators } = create({}); + const spy = jest.fn(); + const subscription = store.state$.subscribe(spy); + mutators.set({ a: 1 }); + mutators.set({ a: 2 }); + subscription.unsubscribe(); + mutators.set({ a: 3 }); + + expect(spy).toHaveBeenCalledTimes(2); + expect(spy.mock.calls[0][0]).toEqual({ a: 1 }); + expect(spy.mock.calls[1][0]).toEqual({ a: 2 }); +}); + +test('multiple subscribers can subscribe', () => { + const { store, mutators } = create({}); + const spy1 = jest.fn(); + const spy2 = jest.fn(); + const subscription1 = store.state$.subscribe(spy1); + const subscription2 = store.state$.subscribe(spy2); + mutators.set({ a: 1 }); + subscription1.unsubscribe(); + mutators.set({ a: 2 }); + subscription2.unsubscribe(); + mutators.set({ a: 3 }); + + expect(spy1).toHaveBeenCalledTimes(1); + expect(spy2).toHaveBeenCalledTimes(2); + expect(spy1.mock.calls[0][0]).toEqual({ a: 1 }); + expect(spy2.mock.calls[0][0]).toEqual({ a: 1 }); + expect(spy2.mock.calls[1][0]).toEqual({ a: 2 }); +}); + +test('creates impure mutators from pure mutators', () => { + const { mutators } = create( + {}, + { + setFoo: () => (bar: any) => ({ foo: bar }), + } + ); + + expect(typeof mutators.setFoo).toBe('function'); +}); + +test('mutators can update state', () => { + const { store, mutators } = create( + { + value: 0, + foo: 'bar', + }, + { + add: (state: any) => (increment: any) => ({ ...state, value: state.value + increment }), + setFoo: (state: any) => (bar: any) => ({ ...state, foo: bar }), + } + ); + + expect(store.get()).toEqual({ + value: 0, + foo: 'bar', + }); + + mutators.add(11); + mutators.setFoo('baz'); + + expect(store.get()).toEqual({ + value: 11, + foo: 'baz', + }); + + mutators.add(-20); + mutators.setFoo('bazooka'); + + expect(store.get()).toEqual({ + value: -9, + foo: 'bazooka', + }); +}); + +test('mutators methods are not bound', () => { + const { store, mutators } = create( + { value: -3 }, + { + add: (state: { value: number }) => (increment: number) => ({ + ...state, + value: state.value + increment, + }), + } + ); + + expect(store.get()).toEqual({ value: -3 }); + mutators.add(4); + expect(store.get()).toEqual({ value: 1 }); +}); + +test('created mutators are saved in store object', () => { + const { store, mutators } = create( + { value: -3 }, + { + add: (state: { value: number }) => (increment: number) => ({ + ...state, + value: state.value + increment, + }), + } + ); + + expect(typeof store.transitions.add).toBe('function'); + mutators.add(5); + expect(store.get()).toEqual({ value: 2 }); +}); + +test('throws when state is modified inline - 1', () => { + const container = createStateContainer({ a: 'b' }, {}); + + let error: TypeError | null = null; + try { + (container.get().a as any) = 'c'; + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(TypeError); +}); + +test('throws when state is modified inline - 2', () => { + const container = createStateContainer({ a: 'b' }, {}); + + let error: TypeError | null = null; + try { + (container.getState().a as any) = 'c'; + } catch (err) { + error = err; + } + + expect(error).toBeInstanceOf(TypeError); +}); + +test('throws when state is modified inline in subscription', done => { + const container = createStateContainer({ a: 'b' }, { set: () => (newState: any) => newState }); + + container.subscribe(value => { + let error: TypeError | null = null; + try { + (value.a as any) = 'd'; + } catch (err) { + error = err; + } + expect(error).toBeInstanceOf(TypeError); + done(); + }); + container.transitions.set({ a: 'c' }); +}); + +describe('selectors', () => { + test('can specify no selectors, or can skip them', () => { + createStateContainer({}, {}); + createStateContainer({}, {}, {}); + }); + + test('selector object is available on .selectors key', () => { + const container1 = createStateContainer({}, {}, {}); + const container2 = createStateContainer({}, {}, { foo: () => () => 123 }); + const container3 = createStateContainer({}, {}, { bar: () => () => 1, baz: () => () => 1 }); + + expect(Object.keys(container1.selectors).sort()).toEqual([]); + expect(Object.keys(container2.selectors).sort()).toEqual(['foo']); + expect(Object.keys(container3.selectors).sort()).toEqual(['bar', 'baz']); + }); + + test('selector without arguments returns correct state slice', () => { + const container = createStateContainer( + { name: 'Oleg' }, + { + changeName: (state: { name: string }) => (name: string) => ({ ...state, name }), + }, + { getName: (state: { name: string }) => () => state.name } + ); + + expect(container.selectors.getName()).toBe('Oleg'); + container.transitions.changeName('Britney'); + expect(container.selectors.getName()).toBe('Britney'); + }); + + test('selector can accept an argument', () => { + const container = createStateContainer( + { + users: { + 1: { + name: 'Darth', + }, + }, + }, + {}, + { + getUser: (state: any) => (id: number) => state.users[id], + } + ); + + expect(container.selectors.getUser(1)).toEqual({ name: 'Darth' }); + expect(container.selectors.getUser(2)).toBe(undefined); + }); + + test('selector can accept multiple arguments', () => { + const container = createStateContainer( + { + users: { + 5: { + name: 'Darth', + surname: 'Vader', + }, + }, + }, + {}, + { + getName: (state: any) => (id: number, which: 'name' | 'surname') => state.users[id][which], + } + ); + + expect(container.selectors.getName(5, 'name')).toEqual('Darth'); + expect(container.selectors.getName(5, 'surname')).toEqual('Vader'); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts new file mode 100644 index 0000000000000..1ef4a1c012817 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container.ts @@ -0,0 +1,89 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject } from 'rxjs'; +import { skip } from 'rxjs/operators'; +import { RecursiveReadonly } from '@kbn/utility-types'; +import { + PureTransitionsToTransitions, + PureTransition, + ReduxLikeStateContainer, + PureSelectorsToSelectors, +} from './types'; + +const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable'; + +const freeze: (value: T) => RecursiveReadonly = + process.env.NODE_ENV !== 'production' + ? (value: T): RecursiveReadonly => { + if (!value) return value as RecursiveReadonly; + if (value instanceof Array) return value as RecursiveReadonly; + if (typeof value === 'object') return Object.freeze({ ...value }) as RecursiveReadonly; + else return value as RecursiveReadonly; + } + : (value: T) => value as RecursiveReadonly; + +export const createStateContainer = < + State, + PureTransitions extends object, + PureSelectors extends object = {} +>( + defaultState: State, + pureTransitions: PureTransitions, + pureSelectors: PureSelectors = {} as PureSelectors +): ReduxLikeStateContainer => { + const data$ = new BehaviorSubject>(freeze(defaultState)); + const state$ = data$.pipe(skip(1)); + const get = () => data$.getValue(); + const container: ReduxLikeStateContainer = { + get, + state$, + getState: () => data$.getValue(), + set: (state: State) => { + data$.next(freeze(state)); + }, + reducer: (state, action) => { + const pureTransition = (pureTransitions as Record>)[ + action.type + ]; + return pureTransition ? freeze(pureTransition(state)(...action.args)) : state; + }, + replaceReducer: nextReducer => (container.reducer = nextReducer), + dispatch: action => data$.next(container.reducer(get(), action)), + transitions: Object.keys(pureTransitions).reduce>( + (acc, type) => ({ ...acc, [type]: (...args: any) => container.dispatch({ type, args }) }), + {} as PureTransitionsToTransitions + ), + selectors: Object.keys(pureSelectors).reduce>( + (acc, selector) => ({ + ...acc, + [selector]: (...args: any) => (pureSelectors as any)[selector](get())(...args), + }), + {} as PureSelectorsToSelectors + ), + addMiddleware: middleware => + (container.dispatch = middleware(container as any)(container.dispatch)), + subscribe: (listener: (state: RecursiveReadonly) => void) => { + const subscription = state$.subscribe(listener); + return () => subscription.unsubscribe(); + }, + [$$observable]: state$, + }; + return container; +}; diff --git a/src/plugins/kibana_utils/public/store/react.test.tsx b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx similarity index 65% rename from src/plugins/kibana_utils/public/store/react.test.tsx rename to src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx index e629e9d0e1257..8f5810f3e147d 100644 --- a/src/plugins/kibana_utils/public/store/react.test.tsx +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx @@ -20,8 +20,17 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { act, Simulate } from 'react-dom/test-utils'; -import { createStore } from './create_store'; -import { createContext } from './react'; +import { createStateContainer } from './create_state_container'; +import { createStateContainerReactHelpers } from './create_state_container_react_helpers'; + +const create = (state: S, transitions: T = {} as T) => { + const pureTransitions = { + set: () => (newState: S) => newState, + ...transitions, + }; + const store = createStateContainer(state, pureTransitions); + return { store, mutators: store.transitions }; +}; let container: HTMLDivElement | null; @@ -36,27 +45,23 @@ afterEach(() => { }); test('can create React context', () => { - const store = createStore({ foo: 'bar' }); - const context = createContext(store); + const context = createStateContainerReactHelpers(); expect(context).toMatchObject({ - Provider: expect.any(Function), - Consumer: expect.any(Function), + Provider: expect.any(Object), + Consumer: expect.any(Object), connect: expect.any(Function), - context: { - Provider: expect.any(Object), - Consumer: expect.any(Object), - }, + context: expect.any(Object), }); }); test(' passes state to ', () => { - const store = createStore({ hello: 'world' }); - const { Provider, Consumer } = createContext(store); + const { store } = create({ hello: 'world' }); + const { Provider, Consumer } = createStateContainerReactHelpers(); ReactDOM.render( - - {({ hello }) => hello} + + {(s: typeof store) => s.get().hello} , container ); @@ -74,8 +79,8 @@ interface Props1 { } test(' passes state to connect()()', () => { - const store = createStore({ hello: 'Bob' }); - const { Provider, connect } = createContext(store); + const { store } = create({ hello: 'Bob' }); + const { Provider, connect } = createStateContainerReactHelpers(); const Demo: React.FC = ({ message, stop }) => ( <> @@ -87,7 +92,7 @@ test(' passes state to connect()()', () => { const DemoConnected = connect(mergeProps)(Demo); ReactDOM.render( - + , container @@ -97,13 +102,13 @@ test(' passes state to connect()()', () => { }); test('context receives Redux store', () => { - const store = createStore({ foo: 'bar' }); - const { Provider, context } = createContext(store); + const { store } = create({ foo: 'bar' }); + const { Provider, context } = createStateContainerReactHelpers(); ReactDOM.render( /* eslint-disable no-shadow */ - - {({ store }) => store.getState().foo} + + {store => store.get().foo} , /* eslint-enable no-shadow */ container @@ -117,16 +122,16 @@ xtest('can use multiple stores in one React app', () => {}); describe('hooks', () => { describe('useStore', () => { test('can select store using useStore hook', () => { - const store = createStore({ foo: 'bar' }); - const { Provider, useStore } = createContext(store); + const { store } = create({ foo: 'bar' }); + const { Provider, useContainer } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { // eslint-disable-next-line no-shadow - const store = useStore(); + const store = useContainer(); return <>{store.get().foo}; }; ReactDOM.render( - + , container @@ -138,15 +143,15 @@ describe('hooks', () => { describe('useState', () => { test('can select state using useState hook', () => { - const store = createStore({ foo: 'qux' }); - const { Provider, useState } = createContext(store); + const { store } = create({ foo: 'qux' }); + const { Provider, useState } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const { foo } = useState(); return <>{foo}; }; ReactDOM.render( - + , container @@ -156,18 +161,23 @@ describe('hooks', () => { }); test('re-renders when state changes', () => { - const store = createStore({ foo: 'bar' }); - const { setFoo } = store.createMutators({ - setFoo: state => foo => ({ ...state, foo }), - }); - const { Provider, useState } = createContext(store); + const { + store, + mutators: { setFoo }, + } = create( + { foo: 'bar' }, + { + setFoo: (state: { foo: string }) => (foo: string) => ({ ...state, foo }), + } + ); + const { Provider, useState } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const { foo } = useState(); return <>{foo}; }; ReactDOM.render( - + , container @@ -181,26 +191,31 @@ describe('hooks', () => { }); }); - describe('useMutations', () => { - test('useMutations hook returns mutations that can update state', () => { - const store = createStore< + describe('useTransitions', () => { + test('useTransitions hook returns mutations that can update state', () => { + const { store } = create< { cnt: number; }, + any + >( { - increment: (value: number) => void; + cnt: 0, + }, + { + increment: (state: { cnt: number }) => (value: number) => ({ + ...state, + cnt: state.cnt + value, + }), } - >({ - cnt: 0, - }); - store.createMutators({ - increment: state => value => ({ ...state, cnt: state.cnt + value }), - }); + ); - const { Provider, useState, useMutators } = createContext(store); + const { Provider, useState, useTransitions } = createStateContainerReactHelpers< + typeof store + >(); const Demo: React.FC<{}> = () => { const { cnt } = useState(); - const { increment } = useMutators(); + const { increment } = useTransitions(); return ( <> {cnt} @@ -210,7 +225,7 @@ describe('hooks', () => { }; ReactDOM.render( - + , container @@ -230,7 +245,7 @@ describe('hooks', () => { describe('useSelector', () => { test('can select deeply nested value', () => { - const store = createStore({ + const { store } = create({ foo: { bar: { baz: 'qux', @@ -238,14 +253,14 @@ describe('hooks', () => { }, }); const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const value = useSelector(selector); return <>{value}; }; ReactDOM.render( - + , container @@ -255,7 +270,7 @@ describe('hooks', () => { }); test('re-renders when state changes', () => { - const store = createStore({ + const { store, mutators } = create({ foo: { bar: { baz: 'qux', @@ -263,14 +278,14 @@ describe('hooks', () => { }, }); const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); const Demo: React.FC<{}> = () => { const value = useSelector(selector); return <>{value}; }; ReactDOM.render( - + , container @@ -278,7 +293,7 @@ describe('hooks', () => { expect(container!.innerHTML).toBe('qux'); act(() => { - store.set({ + mutators.set({ foo: { bar: { baz: 'quux', @@ -290,9 +305,9 @@ describe('hooks', () => { }); test("re-renders only when selector's result changes", async () => { - const store = createStore({ a: 'b', foo: 'bar' }); + const { store, mutators } = create({ a: 'b', foo: 'bar' }); const selector = (state: { foo: string }) => state.foo; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); let cnt = 0; const Demo: React.FC<{}> = () => { @@ -301,7 +316,7 @@ describe('hooks', () => { return <>{value}; }; ReactDOM.render( - + , container @@ -311,24 +326,24 @@ describe('hooks', () => { expect(cnt).toBe(1); act(() => { - store.set({ a: 'c', foo: 'bar' }); + mutators.set({ a: 'c', foo: 'bar' }); }); await new Promise(r => setTimeout(r, 1)); expect(cnt).toBe(1); act(() => { - store.set({ a: 'd', foo: 'bar 2' }); + mutators.set({ a: 'd', foo: 'bar 2' }); }); await new Promise(r => setTimeout(r, 1)); expect(cnt).toBe(2); }); - test('re-renders on same shape object', async () => { - const store = createStore({ foo: { bar: 'baz' } }); + test('does not re-render on same shape object', async () => { + const { store, mutators } = create({ foo: { bar: 'baz' } }); const selector = (state: { foo: any }) => state.foo; - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); let cnt = 0; const Demo: React.FC<{}> = () => { @@ -337,7 +352,7 @@ describe('hooks', () => { return <>{JSON.stringify(value)}; }; ReactDOM.render( - + , container @@ -347,7 +362,14 @@ describe('hooks', () => { expect(cnt).toBe(1); act(() => { - store.set({ foo: { bar: 'baz' } }); + mutators.set({ foo: { bar: 'baz' } }); + }); + + await new Promise(r => setTimeout(r, 1)); + expect(cnt).toBe(1); + + act(() => { + mutators.set({ foo: { bar: 'qux' } }); }); await new Promise(r => setTimeout(r, 1)); @@ -355,10 +377,15 @@ describe('hooks', () => { }); test('can set custom comparator function to prevent re-renders on deep equality', async () => { - const store = createStore({ foo: { bar: 'baz' } }); + const { store, mutators } = create( + { foo: { bar: 'baz' } }, + { + set: () => (newState: { foo: { bar: string } }) => newState, + } + ); const selector = (state: { foo: any }) => state.foo; const comparator = (prev: any, curr: any) => JSON.stringify(prev) === JSON.stringify(curr); - const { Provider, useSelector } = createContext(store); + const { Provider, useSelector } = createStateContainerReactHelpers(); let cnt = 0; const Demo: React.FC<{}> = () => { @@ -367,7 +394,7 @@ describe('hooks', () => { return <>{JSON.stringify(value)}; }; ReactDOM.render( - + , container @@ -377,7 +404,7 @@ describe('hooks', () => { expect(cnt).toBe(1); act(() => { - store.set({ foo: { bar: 'baz' } }); + mutators.set({ foo: { bar: 'baz' } }); }); await new Promise(r => setTimeout(r, 1)); diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts new file mode 100644 index 0000000000000..e94165cc48376 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import * as React from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import defaultComparator from 'fast-deep-equal'; +import { Comparator, Connect, StateContainer, UnboxState } from './types'; + +const { useContext, useLayoutEffect, useRef, createElement: h } = React; + +export const createStateContainerReactHelpers = >() => { + const context = React.createContext(null as any); + + const useContainer = (): Container => useContext(context); + + const useState = (): UnboxState => { + const { state$, get } = useContainer(); + const value = useObservable(state$, get()); + return value; + }; + + const useTransitions = () => useContainer().transitions; + + const useSelector = ( + selector: (state: UnboxState) => Result, + comparator: Comparator = defaultComparator + ): Result => { + const { state$, get } = useContainer(); + const lastValueRef = useRef(get()); + const [value, setValue] = React.useState(() => { + const newValue = selector(get()); + lastValueRef.current = newValue; + return newValue; + }); + useLayoutEffect(() => { + const subscription = state$.subscribe((currentState: UnboxState) => { + const newValue = selector(currentState); + if (!comparator(lastValueRef.current, newValue)) { + lastValueRef.current = newValue; + setValue(newValue); + } + }); + return () => subscription.unsubscribe(); + }, [state$, comparator]); + return value; + }; + + const connect: Connect> = mapStateToProp => component => props => + h(component, { ...useSelector(mapStateToProp), ...props } as any); + + return { + Provider: context.Provider, + Consumer: context.Consumer, + context, + useContainer, + useState, + useTransitions, + useSelector, + connect, + }; +}; diff --git a/src/plugins/kibana_utils/public/store/index.ts b/src/plugins/kibana_utils/public/state_containers/index.ts similarity index 86% rename from src/plugins/kibana_utils/public/store/index.ts rename to src/plugins/kibana_utils/public/state_containers/index.ts index 468e8ab8c5ade..43e204ecb79f7 100644 --- a/src/plugins/kibana_utils/public/store/index.ts +++ b/src/plugins/kibana_utils/public/state_containers/index.ts @@ -17,5 +17,6 @@ * under the License. */ -export * from './create_store'; -export * from './react'; +export * from './types'; +export * from './create_state_container'; +export * from './create_state_container_react_helpers'; diff --git a/src/plugins/kibana_utils/public/state_containers/types.ts b/src/plugins/kibana_utils/public/state_containers/types.ts new file mode 100644 index 0000000000000..e0a1a18972635 --- /dev/null +++ b/src/plugins/kibana_utils/public/state_containers/types.ts @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { Ensure, RecursiveReadonly } from '@kbn/utility-types'; + +export interface TransitionDescription { + type: Type; + args: Args; +} +export type Transition = (...args: Args) => State; +export type PureTransition = ( + state: RecursiveReadonly +) => Transition; +export type EnsurePureTransition = Ensure>; +export type PureTransitionToTransition> = ReturnType; +export type PureTransitionsToTransitions = { + [K in keyof T]: PureTransitionToTransition>; +}; + +export interface BaseStateContainer { + get: () => RecursiveReadonly; + set: (state: State) => void; + state$: Observable>; +} + +export interface StateContainer< + State, + PureTransitions extends object, + PureSelectors extends object = {} +> extends BaseStateContainer { + transitions: Readonly>; + selectors: Readonly>; +} + +export interface ReduxLikeStateContainer< + State, + PureTransitions extends object, + PureSelectors extends object = {} +> extends StateContainer { + getState: () => RecursiveReadonly; + reducer: Reducer>; + replaceReducer: (nextReducer: Reducer>) => void; + dispatch: (action: TransitionDescription) => void; + addMiddleware: (middleware: Middleware>) => void; + subscribe: (listener: (state: RecursiveReadonly) => void) => () => void; +} + +export type Dispatch = (action: T) => void; + +export type Middleware = ( + store: Pick, 'getState' | 'dispatch'> +) => ( + next: (action: TransitionDescription) => TransitionDescription | any +) => Dispatch; + +export type Reducer = (state: State, action: TransitionDescription) => State; + +export type UnboxState< + Container extends StateContainer +> = Container extends StateContainer ? T : never; +export type UnboxTransitions< + Container extends StateContainer +> = Container extends StateContainer ? T : never; + +export type Selector = (...args: Args) => Result; +export type PureSelector = ( + state: State +) => Selector; +export type EnsurePureSelector = Ensure>; +export type PureSelectorToSelector> = ReturnType< + EnsurePureSelector +>; +export type PureSelectorsToSelectors = { + [K in keyof T]: PureSelectorToSelector>; +}; + +export type Comparator = (previous: Result, current: Result) => boolean; + +export type MapStateToProps = (state: State) => StateProps; +export type Connect = ( + mapStateToProp: MapStateToProps> +) => (component: React.ComponentType) => React.FC>; diff --git a/src/plugins/kibana_utils/public/store/create_store.test.ts b/src/plugins/kibana_utils/public/store/create_store.test.ts deleted file mode 100644 index cfdeb76254003..0000000000000 --- a/src/plugins/kibana_utils/public/store/create_store.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createStore } from './create_store'; - -test('can create store', () => { - const store = createStore({}); - expect(store).toMatchObject({ - get: expect.any(Function), - set: expect.any(Function), - state$: expect.any(Object), - createMutators: expect.any(Function), - mutators: expect.any(Object), - redux: { - getState: expect.any(Function), - dispatch: expect.any(Function), - subscribe: expect.any(Function), - }, - }); -}); - -test('can set default state', () => { - const defaultState = { - foo: 'bar', - }; - const store = createStore(defaultState); - expect(store.get()).toEqual(defaultState); - expect(store.redux.getState()).toEqual(defaultState); -}); - -test('can set state', () => { - const defaultState = { - foo: 'bar', - }; - const newState = { - foo: 'baz', - }; - const store = createStore(defaultState); - - store.set(newState); - - expect(store.get()).toEqual(newState); - expect(store.redux.getState()).toEqual(newState); -}); - -test('does not shallow merge states', () => { - const defaultState = { - foo: 'bar', - }; - const newState = { - foo2: 'baz', - }; - const store = createStore(defaultState); - - store.set(newState); - - expect(store.get()).toEqual(newState); - expect(store.redux.getState()).toEqual(newState); -}); - -test('can subscribe and unsubscribe to state changes', () => { - const store = createStore({}); - const spy = jest.fn(); - const subscription = store.state$.subscribe(spy); - store.set({ a: 1 }); - store.set({ a: 2 }); - subscription.unsubscribe(); - store.set({ a: 3 }); - - expect(spy).toHaveBeenCalledTimes(2); - expect(spy.mock.calls[0][0]).toEqual({ a: 1 }); - expect(spy.mock.calls[1][0]).toEqual({ a: 2 }); -}); - -test('multiple subscribers can subscribe', () => { - const store = createStore({}); - const spy1 = jest.fn(); - const spy2 = jest.fn(); - const subscription1 = store.state$.subscribe(spy1); - const subscription2 = store.state$.subscribe(spy2); - store.set({ a: 1 }); - subscription1.unsubscribe(); - store.set({ a: 2 }); - subscription2.unsubscribe(); - store.set({ a: 3 }); - - expect(spy1).toHaveBeenCalledTimes(1); - expect(spy2).toHaveBeenCalledTimes(2); - expect(spy1.mock.calls[0][0]).toEqual({ a: 1 }); - expect(spy2.mock.calls[0][0]).toEqual({ a: 1 }); - expect(spy2.mock.calls[1][0]).toEqual({ a: 2 }); -}); - -test('creates impure mutators from pure mutators', () => { - const store = createStore({}); - const mutators = store.createMutators({ - setFoo: _ => bar => ({ foo: bar }), - }); - - expect(typeof mutators.setFoo).toBe('function'); -}); - -test('mutators can update state', () => { - const store = createStore({ - value: 0, - foo: 'bar', - }); - const mutators = store.createMutators({ - add: state => increment => ({ ...state, value: state.value + increment }), - setFoo: state => bar => ({ ...state, foo: bar }), - }); - - expect(store.get()).toEqual({ - value: 0, - foo: 'bar', - }); - - mutators.add(11); - mutators.setFoo('baz'); - - expect(store.get()).toEqual({ - value: 11, - foo: 'baz', - }); - - mutators.add(-20); - mutators.setFoo('bazooka'); - - expect(store.get()).toEqual({ - value: -9, - foo: 'bazooka', - }); -}); - -test('mutators methods are not bound', () => { - const store = createStore({ value: -3 }); - const { add } = store.createMutators({ - add: state => increment => ({ ...state, value: state.value + increment }), - }); - - expect(store.get()).toEqual({ value: -3 }); - add(4); - expect(store.get()).toEqual({ value: 1 }); -}); - -test('created mutators are saved in store object', () => { - const store = createStore< - any, - { - add: (increment: number) => void; - } - >({ value: -3 }); - - store.createMutators({ - add: state => increment => ({ ...state, value: state.value + increment }), - }); - - expect(typeof store.mutators.add).toBe('function'); - store.mutators.add(5); - expect(store.get()).toEqual({ value: 2 }); -}); diff --git a/src/plugins/kibana_utils/public/store/create_store.ts b/src/plugins/kibana_utils/public/store/create_store.ts deleted file mode 100644 index 315523360f92d..0000000000000 --- a/src/plugins/kibana_utils/public/store/create_store.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { createStore as createReduxStore, Reducer } from 'redux'; -import { Subject, Observable } from 'rxjs'; -import { AppStore, Mutators, PureMutators } from './types'; - -const SET = '__SET__'; - -export const createStore = < - State extends {}, - StateMutators extends Mutators> = {} ->( - defaultState: State -): AppStore => { - const pureMutators: PureMutators = {}; - const mutators: StateMutators = {} as StateMutators; - const reducer: Reducer = (state, action) => { - const pureMutator = pureMutators[action.type]; - if (pureMutator) { - return pureMutator(state)(...action.args); - } - - switch (action.type) { - case SET: - return action.state; - default: - return state; - } - }; - const redux = createReduxStore(reducer, defaultState as any); - - const get = redux.getState; - - const set = (state: State) => - redux.dispatch({ - type: SET, - state, - }); - - const state$ = new Subject(); - redux.subscribe(() => { - state$.next(get()); - }); - - const createMutators: AppStore['createMutators'] = newPureMutators => { - const result: Mutators = {}; - for (const type of Object.keys(newPureMutators)) { - result[type] = (...args) => { - redux.dispatch({ - type, - args, - }); - }; - } - Object.assign(pureMutators, newPureMutators); - Object.assign(mutators, result); - return result; - }; - - return { - get, - set, - redux, - state$: (state$ as unknown) as Observable, - createMutators, - mutators, - }; -}; diff --git a/src/plugins/kibana_utils/public/store/observable_selector.ts b/src/plugins/kibana_utils/public/store/observable_selector.ts deleted file mode 100644 index 6ba6f42296a6c..0000000000000 --- a/src/plugins/kibana_utils/public/store/observable_selector.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Observable, BehaviorSubject } from 'rxjs'; - -export type Selector = (state: State) => Result; -export type Comparator = (previous: Result, current: Result) => boolean; -export type Unsubscribe = () => void; - -const defaultComparator: Comparator = (previous, current) => previous === current; - -export const observableSelector = ( - state: State, - state$: Observable, - selector: Selector, - comparator: Comparator = defaultComparator -): [Observable, Unsubscribe] => { - let previousResult: Result = selector(state); - const result$ = new BehaviorSubject(previousResult); - - const subscription = state$.subscribe(value => { - const result = selector(value); - const isEqual: boolean = comparator(previousResult, result); - if (!isEqual) { - result$.next(result); - } - previousResult = result; - }); - - return [(result$ as unknown) as Observable, subscription.unsubscribe]; -}; diff --git a/src/plugins/kibana_utils/public/store/react.ts b/src/plugins/kibana_utils/public/store/react.ts deleted file mode 100644 index 00861b2b0b8fe..0000000000000 --- a/src/plugins/kibana_utils/public/store/react.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import * as React from 'react'; -import { Provider as ReactReduxProvider, connect as reactReduxConnect } from 'react-redux'; -import { Store } from 'redux'; -import { AppStore, Mutators, PureMutators } from './types'; -import { observableSelector, Selector, Comparator } from './observable_selector'; -// TODO: Below import is temporary, use `react-use` lib instead. -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { useObservable } from '../../../kibana_react/public/util/use_observable'; - -const { useMemo, useLayoutEffect, useContext, createElement, Fragment } = React; - -/** - * @note - * Types in `react-redux` seem to be quite off compared to reality - * that's why a lot of `any`s below. - */ - -export interface ConsumerProps { - children: (state: State) => React.ReactChild; -} - -export type MapStateToProps = (state: State) => StateProps; - -// TODO: `Omit` is generally part of TypeScript, but it currently does not exist in our build. -type Omit = Pick>; -export type Connect = ( - mapStateToProp: MapStateToProps> -) => (component: React.ComponentType) => React.FC>; - -interface ReduxContextValue { - store: Store; -} - -const mapDispatchToProps = () => ({}); -const mergeProps: any = (stateProps: any, dispatchProps: any, ownProps: any) => ({ - ...ownProps, - ...stateProps, - ...dispatchProps, -}); - -export const createContext = < - State extends {}, - StateMutators extends Mutators> = {} ->( - store: AppStore -) => { - const { redux } = store; - (redux as any).__appStore = store; - const context = React.createContext({ store: redux }); - - const useStore = (): AppStore => { - // eslint-disable-next-line no-shadow - const { store } = useContext(context); - return (store as any).__appStore; - }; - - const useState = (): State => { - const { state$, get } = useStore(); - const state = useObservable(state$, get()); - return state; - }; - - const useMutators = (): StateMutators => useStore().mutators; - - const useSelector = ( - selector: Selector, - comparator?: Comparator - ): Result => { - const { state$, get } = useStore(); - /* eslint-disable react-hooks/exhaustive-deps */ - const [observable$, unsubscribe] = useMemo( - () => observableSelector(get(), state$, selector, comparator), - [state$] - ); - /* eslint-enable react-hooks/exhaustive-deps */ - useLayoutEffect(() => unsubscribe, [observable$, unsubscribe]); - const value = useObservable(observable$, selector(get())); - return value; - }; - - const Provider: React.FC<{}> = ({ children }) => - createElement(ReactReduxProvider, { - store: redux, - context, - children, - } as any); - - const Consumer: React.FC> = ({ children }) => { - const state = useState(); - return createElement(Fragment, { children: children(state) }); - }; - - const options: any = { context }; - const connect: Connect = mapStateToProps => - reactReduxConnect(mapStateToProps, mapDispatchToProps, mergeProps, options) as any; - - return { - Provider, - Consumer, - connect, - context, - useStore, - useState, - useMutators, - useSelector, - }; -}; diff --git a/yarn.lock b/yarn.lock index b4960a6cd01e0..dcaaa3da9fd75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6809,6 +6809,11 @@ bounce@1.x.x: boom "7.x.x" hoek "5.x.x" +bowser@^1.7.3: + version "1.9.4" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a" + integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ== + boxen@^1.2.1, boxen@^1.2.2: version "1.3.0" resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" @@ -8719,6 +8724,13 @@ copy-to-clipboard@^3.0.8: dependencies: toggle-selection "^1.0.3" +copy-to-clipboard@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467" + integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w== + dependencies: + toggle-selection "^1.0.6" + copy-webpack-plugin@^5.0.4: version "5.0.4" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.0.4.tgz#c78126f604e24f194c6ec2f43a64e232b5d43655" @@ -9096,6 +9108,14 @@ css-color-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= +css-in-js-utils@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99" + integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA== + dependencies: + hyphenate-style-name "^1.0.2" + isobject "^3.0.1" + css-loader@2.1.1, css-loader@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" @@ -9181,6 +9201,14 @@ css-tree@1.0.0-alpha.29: mdn-data "~1.1.0" source-map "^0.5.3" +css-tree@^1.0.0-alpha.28: + version "1.0.0-alpha.39" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" + integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== + dependencies: + mdn-data "2.0.6" + source-map "^0.6.1" + css-url-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec" @@ -9252,7 +9280,7 @@ cssstyle@^2.0.0: dependencies: cssom "~0.3.6" -csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7: +csstype@^2.2.0, csstype@^2.5.5, csstype@^2.5.7, csstype@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5" integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ== @@ -10998,6 +11026,13 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error-stack-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.4.tgz#a757397dc5d9de973ac9a5d7d4e8ade7cfae9101" + integrity sha512-fZ0KkoxSjLFmhW5lHbUT3tLwy3nX1qEzMYo8koY1vrsAco53CMT1djnBSeC/wUjTEZRhZl9iRw7PaMaxfJ4wzQ== + dependencies: + stackframe "^1.1.0" + error@^7.0.0, error@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" @@ -12174,6 +12209,11 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + fast-diff@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" @@ -12240,6 +12280,11 @@ fast-stream-to-buffer@^1.0.0: dependencies: end-of-stream "^1.4.1" +fastest-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028" + integrity sha1-kSLUBtTJ2YvqZEpraFPVh0uHsCg= + fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" @@ -15158,6 +15203,11 @@ hyperlinker@^1.0.0: resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ== +hyphenate-style-name@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" + integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== + i18n-iso-countries@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-4.3.1.tgz#f110a8824ce14edbb0eb8f3b0bd817ff950af37c" @@ -15437,6 +15487,14 @@ ini@^1.2.0, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== +inline-style-prefixer@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911" + integrity sha512-N8nVhwfYga9MiV9jWlwfdj1UDIaZlBFu4cJSJkIr7tZX7sHpHhGR5su1qdpW+7KPL8ISTvCIkcaFi/JdBknvPg== + dependencies: + bowser "^1.7.3" + css-in-js-utils "^2.0.0" + inline-style@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/inline-style/-/inline-style-2.0.0.tgz#2fa9cf624596a8109355b925094e138bbd5ea29b" @@ -18958,6 +19016,11 @@ mdast-add-list-metadata@1.0.1: dependencies: unist-util-visit-parents "1.1.2" +mdn-data@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" + integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== + mdn-data@~1.1.0: version "1.1.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" @@ -19828,6 +19891,20 @@ nan@^2.10.0, nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nano-css@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.2.1.tgz#73b8470fa40b028a134d3393ae36bbb34b9fa332" + integrity sha512-T54okxMAha0+de+W8o3qFtuWhTxYvqQh2ku1cYEqTTP9mR62nWV2lLK9qRuAGWmoaYWhU7K4evT9Lc1iF65wuw== + dependencies: + css-tree "^1.0.0-alpha.28" + csstype "^2.5.5" + fastest-stable-stringify "^1.0.1" + inline-style-prefixer "^4.0.0" + rtl-css-js "^1.9.0" + sourcemap-codec "^1.4.1" + stacktrace-js "^2.0.0" + stylis "3.5.0" + nano-time@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" @@ -23530,6 +23607,21 @@ react-transition-group@^2.2.1: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" +react-use@^13.10.2: + version "13.10.2" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.10.2.tgz#4250d258ca9068662943299c01794a136408c8e9" + integrity sha512-z3VFSiPHW6arViGVnajO7YKY5OD+Z9LWcImoJdYHkau23cLSoTctxM3XENLpGxjhJlHaYiQZ6pPgq7pwGTqSZA== + dependencies: + copy-to-clipboard "^3.2.0" + nano-css "^5.2.1" + react-fast-compare "^2.0.4" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^2.1.0" + ts-easing "^0.2.0" + tslib "^1.10.0" + react-virtualized@^9.18.5: version "9.20.1" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.20.1.tgz#02dc08fe9070386b8c48e2ac56bce7af0208d22d" @@ -24892,6 +24984,13 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +rtl-css-js@^1.9.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.13.1.tgz#80deabf6e8f36d6767d495cd3eb60fecb20c67e1" + integrity sha512-jgkIDj6Xi25kAEm5oYM3ZMFiOQhpLEcXi2LY/6bVr91cVz73hciHKneL5AMVPxOcks/JuizSaaNsvNRkeAWe3w== + dependencies: + "@babel/runtime" "^7.1.2" + run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -25189,6 +25288,11 @@ scoped-regex@^1.0.0: resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8" integrity sha1-o0a7Gs1CB65wvXwMfKnlZra63bg= +screenfull@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.0.0.tgz#5c2010c0e84fd4157bf852877698f90b8cbe96f6" + integrity sha512-yShzhaIoE9OtOhWVyBBffA6V98CDCoyHTsp8228blmqYy1Z5bddzE/4FPiJKlr8DVR4VBiiUyfPzIQPIYDkeMA== + script-loader@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/script-loader/-/script-loader-0.7.2.tgz#2016db6f86f25f5cf56da38915d83378bb166ba7" @@ -25421,6 +25525,11 @@ set-getter@^0.1.0: dependencies: to-object-path "^0.3.0" +set-harmonic-interval@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249" + integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g== + set-immediate-shim@^1.0.0, set-immediate-shim@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" @@ -25897,6 +26006,11 @@ source-map@0.1.32: dependencies: amdefine ">=0.0.4" +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= + "source-map@>= 0.1.2": version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" @@ -25933,6 +26047,11 @@ source-map@~0.2.0: dependencies: amdefine ">=0.0.4" +sourcemap-codec@^1.4.1: + version "1.4.6" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" + integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg== + space-separated-tokens@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412" @@ -26134,6 +26253,13 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stack-generator@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.4.tgz#027513eab2b195bbb43b9c8360ba2dd0ab54de09" + integrity sha512-ha1gosTNcgxwzo9uKTQ8zZ49aUp5FIUW58YHFxCqaAHtE0XqBg0chGFYA1MfmW//x1KWq3F4G7Ug7bJh4RiRtg== + dependencies: + stackframe "^1.1.0" + stack-trace@0.0.10, stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -26144,6 +26270,11 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" integrity sha1-1PM6tU6OOHeLDKXP07OvsS22hiA= +stackframe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.0.tgz#e3fc2eb912259479c9822f7d1f1ff365bd5cbc83" + integrity sha512-Vx6W1Yvy+AM1R/ckVwcHQHV147pTPBKWCRLrXMuPrFVfvBUc3os7PR1QLIWCMhPpRg5eX9ojzbQIMLGBwyLjqg== + stackman@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/stackman/-/stackman-4.0.0.tgz#3ccdc8682fee36373ed2492dc3dad546eb44647d" @@ -26155,6 +26286,23 @@ stackman@^4.0.0: error-callsites "^2.0.2" load-source-map "^1.0.0" +stacktrace-gps@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.3.tgz#b89f84cc13bb925b96607e737b617c8715facf57" + integrity sha512-51Rr7dXkyFUKNmhY/vqZWK+EvdsfFSRiQVtgHTFlAdNIYaDD7bVh21yBHXaNWAvTD+w+QSjxHg7/v6Tz4veExA== + dependencies: + source-map "0.5.6" + stackframe "^1.1.0" + +stacktrace-js@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.1.tgz#ebdb0e9a16e6f171f96ca7878404e7f15c3d42ba" + integrity sha512-13oDNgBSeWtdGa4/2BycNyKqe+VktCoJ8VLx4pDoJkwGGJVtiHdfMOAj3aW9xTi8oR2v34z9IcvfCvT6XNdNAw== + dependencies: + error-stack-parser "^2.0.4" + stack-generator "^2.0.4" + stacktrace-gps "^3.0.3" + state-toggle@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425" @@ -26677,6 +26825,11 @@ stylis-rule-sheet@^0.0.10: resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430" integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw== +stylis@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" + integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw== + stylus-lookup@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/stylus-lookup/-/stylus-lookup-3.0.2.tgz#c9eca3ff799691020f30b382260a67355fefdddd" @@ -27520,7 +27673,7 @@ to-through@^2.0.0: dependencies: through2 "^2.0.3" -toggle-selection@^1.0.3: +toggle-selection@^1.0.3, toggle-selection@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= @@ -27666,6 +27819,11 @@ ts-dedent@^1.1.0: resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-1.1.0.tgz#67983940793183dc7c7f820acb66ba02cdc33c6e" integrity sha512-CVCvDwMBWZKjDxpN3mU/Dx1v3k+sJgE8nrhXcC9vRopRfoa7vVzilNvHEAUi5jQnmFHpnxDx5jZdI1TpG8ny2g== +ts-easing@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" + integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== + ts-invariant@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.2.1.tgz#3d587f9d6e3bded97bf9ec17951dd9814d5a9d3f" @@ -27721,7 +27879,7 @@ tsd@^0.7.4: typescript "^3.0.1" update-notifier "^2.5.0" -tslib@^1: +tslib@^1, tslib@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== From 6af9f9bea601af976859eae125d4f0a7b9602bdc Mon Sep 17 00:00:00 2001 From: Shahzad Date: Fri, 6 Dec 2019 19:55:57 +0100 Subject: [PATCH 07/26] update columns (#51892) --- .../__snapshots__/ping_list.test.tsx.snap | 6 +- .../__tests__/ping_list.test.tsx | 4 +- .../components/functional/ping_list/index.tsx | 7 +++ .../functional/{ => ping_list}/ping_list.tsx | 62 +++++++++---------- 4 files changed, 42 insertions(+), 37 deletions(-) rename x-pack/legacy/plugins/uptime/public/components/functional/{ => ping_list}/__tests__/__snapshots__/ping_list.test.tsx.snap (99%) rename x-pack/legacy/plugins/uptime/public/components/functional/{ => ping_list}/__tests__/ping_list.test.tsx (98%) create mode 100644 x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx rename x-pack/legacy/plugins/uptime/public/components/functional/{ => ping_list}/ping_list.tsx (88%) diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/ping_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap similarity index 99% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/ping_list.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap index 9c6d2e2f495c1..5f60ce38500c8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/ping_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/__snapshots__/ping_list.test.tsx.snap @@ -121,8 +121,7 @@ exports[`PingList component renders sorted list without errors 1`] = ` "render": [Function], }, Object { - "align": "left", - "dataType": "number", + "align": "center", "field": "observer.geo.name", "name": "Location", "render": [Function], @@ -140,9 +139,10 @@ exports[`PingList component renders sorted list without errors 1`] = ` "render": [Function], }, Object { - "align": "left", + "align": "right", "field": "error.type", "name": "Error type", + "render": [Function], }, Object { "align": "right", diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/ping_list.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx similarity index 98% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/ping_list.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx index 46da7e5233354..43adc4da85f32 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/ping_list.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/__tests__/ping_list.test.tsx @@ -6,10 +6,10 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { PingResults, Ping } from '../../../../common/graphql/types'; +import { PingResults, Ping } from '../../../../../common/graphql/types'; import { PingListComponent, AllLocationOption, toggleDetails } from '../ping_list'; import { EuiComboBoxOptionProps } from '@elastic/eui'; -import { ExpandedRowMap } from '../monitor_list/types'; +import { ExpandedRowMap } from '../../monitor_list/types'; describe('PingList component', () => { let pingList: { allPings: PingResults }; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx new file mode 100644 index 0000000000000..e57b229dfd973 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/index.tsx @@ -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 * from './ping_list'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx similarity index 88% rename from x-pack/legacy/plugins/uptime/public/components/functional/ping_list.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx index fb7c0a5af1e7f..0a97b596a7a71 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/ping_list.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/ping_list/ping_list.tsx @@ -24,13 +24,13 @@ import moment from 'moment'; import React, { Fragment, useEffect, useState } from 'react'; // @ts-ignore formatNumber import { formatNumber } from '@elastic/eui/lib/services/format'; -import { Ping, PingResults } from '../../../common/graphql/types'; -import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../lib/helper'; -import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../higher_order'; -import { pingsQuery } from '../../queries'; -import { LocationName } from './location_name'; -import { Criteria, Pagination } from './monitor_list'; -import { PingListExpandedRowComponent } from './ping_list/expanded_row'; +import { Ping, PingResults } from '../../../../common/graphql/types'; +import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; +import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order'; +import { pingsQuery } from '../../../queries'; +import { LocationName } from './../location_name'; +import { Criteria, Pagination } from './../monitor_list'; +import { PingListExpandedRowComponent } from './expanded_row'; interface PingListQueryResult { allPings?: PingResults; @@ -83,6 +83,10 @@ export const PingListComponent = ({ }: Props) => { const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState({}); + useEffect(() => { + onUpdateApp(); + }, [selectedOption]); + const statusOptions = [ { text: i18n.translate('xpack.uptime.pingList.statusOptions.allStatusOptionLabel', { @@ -141,8 +145,7 @@ export const PingListComponent = ({ ), }, { - align: 'left', - dataType: 'number', + align: 'center', field: 'observer.geo.name', name: i18n.translate('xpack.uptime.pingList.locationNameColumnLabel', { defaultMessage: 'Location', @@ -170,36 +173,31 @@ export const PingListComponent = ({ }), }, { - align: 'left', + align: 'right', field: 'error.type', name: i18n.translate('xpack.uptime.pingList.errorTypeColumnLabel', { defaultMessage: 'Error type', }), + render: (error: string) => error ?? '-', }, ]; - useEffect(() => { - onUpdateApp(); - }, [selectedOption]); - let pings: Ping[] = []; - if (data && data.allPings && data.allPings.pings) { - pings = data.allPings.pings; - const hasStatus: boolean = pings.reduce( - (hasHttpStatus: boolean, currentPing: Ping) => - hasHttpStatus || !!get(currentPing, 'http.response.status_code'), - false - ); - if (hasStatus) { - columns.push({ - field: 'http.response.status_code', - // @ts-ignore "align" property missing on type definition for column type - align: 'right', - name: i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', { - defaultMessage: 'Response code', - }), - render: (statusCode: string) => {statusCode}, - }); - } + + const pings: Ping[] = data?.allPings?.pings ?? []; + + const hasStatus: boolean = pings.some( + (currentPing: Ping) => !!currentPing?.http?.response?.status_code + ); + if (hasStatus) { + columns.push({ + field: 'http.response.status_code', + align: 'right', + name: i18n.translate('xpack.uptime.pingList.responseCodeColumnLabel', { + defaultMessage: 'Response code', + }), + render: (statusCode: string) => {statusCode}, + }); } + columns.push({ align: 'right', width: '40px', From e17539c5da78e1567804218c6beb18048595cea2 Mon Sep 17 00:00:00 2001 From: Spencer Date: Fri, 6 Dec 2019 12:27:06 -0700 Subject: [PATCH 08/26] [ci/pipeline/reportFailures] when aborted, run with --no-github-update (#52355) --- vars/kibanaPipeline.groovy | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 5b3cd071316e6..77907a07addd1 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -262,10 +262,13 @@ def buildXpack() { } def runErrorReporter() { + def status = buildUtils.getBuildStatus() + def dryRun = status != "ABORTED" ? "" : "--no-github-update" + bash( """ source src/dev/ci_setup/setup_env.sh - node scripts/report_failed_tests + node scripts/report_failed_tests ${dryRun} """, "Report failed tests, if necessary" ) From c3ddb53c660139977673a8103effe7f9bdc42bda Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 6 Dec 2019 12:31:19 -0700 Subject: [PATCH 09/26] [SIEM] Adds support for specifying default filters to StatefulEventsViewer (#52413) ## Summary Finishes plumbing through the `defaultFilters` prop on the `StatefuleEventsViewer` component so that your view will always be constrained by a specified filter. Also adds an example of doing so to the current WIP `SignalsTable`. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [ ] ~This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~ - [ ] ~Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~ - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ - [ ] ~[Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios~ - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ ### For maintainers - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ --- .../public/components/events_viewer/index.tsx | 7 +-- ...default_headers.tsx => default_config.tsx} | 47 +++++++++++++++++++ .../signals/default_model.tsx | 13 ----- .../pages/detection_engine/signals/index.tsx | 8 +++- 4 files changed, 57 insertions(+), 18 deletions(-) rename x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/{default_headers.tsx => default_config.tsx} (67%) delete mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx index 613861a4c905c..21292e4ac3254 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.tsx @@ -83,6 +83,7 @@ const StatefulEventsViewerComponent = React.memo( createTimeline, columns, dataProviders, + defaultFilters = [], defaultModel, defaultIndices, deleteEventQuery, @@ -158,7 +159,7 @@ const StatefulEventsViewerComponent = React.memo( id={id} dataProviders={dataProviders!} end={end} - filters={filters} + filters={[...filters, ...defaultFilters]} headerFilterGroup={headerFilterGroup} indexPattern={indexPatterns ?? { fields: [], title: '' }} isLive={isLive} @@ -201,7 +202,7 @@ const makeMapStateToProps = () => { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getEvents = timelineSelectors.getEventsByIdSelector(); - const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { + const mapStateToProps = (state: State, { id, defaultFilters = [], defaultModel }: OwnProps) => { const input: inputsModel.InputsRange = getInputsTimeline(state); const events: TimelineModel = getEvents(state, id) ?? defaultModel; const { columns, dataProviders, itemsPerPage, itemsPerPageOptions, kqlMode, sort } = events; @@ -209,7 +210,7 @@ const makeMapStateToProps = () => { return { columns, dataProviders, - filters: getGlobalFiltersQuerySelector(state), + filters: [...getGlobalFiltersQuerySelector(state), ...defaultFilters], id, isLive: input.policy.kind === 'interval', itemsPerPage, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_headers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx similarity index 67% rename from x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_headers.tsx rename to x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx index d6bfcd80b9956..e90487a3b023c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_headers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_config.tsx @@ -12,6 +12,48 @@ import { } from '../../../components/timeline/body/helpers'; import * as i18n from './translations'; +import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model'; +import { esFilters } from '../../../../../../../../src/plugins/data/common/es_query'; + +export const signalsOpenFilters: esFilters.Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.status', + params: { + query: 'open', + }, + }, + query: { + match_phrase: { + 'signal.status': 'open', + }, + }, + }, +]; + +export const signalsClosedFilters: esFilters.Filter[] = [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.status', + params: { + query: 'closed', + }, + }, + query: { + match_phrase: { + 'signal.status': 'closed', + }, + }, + }, +]; export const signalsHeaders: ColumnHeader[] = [ { @@ -77,3 +119,8 @@ export const signalsHeaders: ColumnHeader[] = [ width: DEFAULT_DATE_COLUMN_MIN_WIDTH, }, ]; + +export const signalsDefaultModel: SubsetTimelineModel = { + ...timelineDefaults, + columns: signalsHeaders, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx deleted file mode 100644 index bb1f806d67c03..0000000000000 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/default_model.tsx +++ /dev/null @@ -1,13 +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 { signalsHeaders } from './default_headers'; -import { SubsetTimelineModel, timelineDefaults } from '../../../store/timeline/model'; - -export const signalsDefaultModel: SubsetTimelineModel = { - ...timelineDefaults, - columns: signalsHeaders, -}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx index edc7ed133d10c..ca178db9cd97f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/signals/index.tsx @@ -12,7 +12,7 @@ import { GlobalTime } from '../../../containers/global_time'; import { StatefulEventsViewer } from '../../../components/events_viewer'; import * as i18n from './translations'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; -import { signalsDefaultModel } from './default_model'; +import { signalsClosedFilters, signalsDefaultModel, signalsOpenFilters } from './default_config'; const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; const FILTER_OPEN = 'open'; @@ -37,7 +37,10 @@ export const SignalsTableFilterGroup = React.memo( setFilterGroup(FILTER_CLOSED)} + onClick={() => { + setFilterGroup(FILTER_CLOSED); + onFilterGroupChanged(FILTER_CLOSED); + }} > {'Closed signals'} @@ -62,6 +65,7 @@ export const SignalsTable = React.memo(() => { {({ to, from, setQuery, deleteQuery, isInitializing }) => ( Date: Fri, 6 Dec 2019 14:55:16 -0500 Subject: [PATCH 10/26] Add Endpoint plugin and Resolver embeddable (#51994) * Add functional tests for plugins to x-pack (so we can do a functional test of the Resolver embeddable) * Add Endpoint plugin * Add Resolver embeddable * Test that Resolver embeddable can be rendered --- x-pack/.i18nrc.json | 9 +-- x-pack/plugins/endpoint/kibana.json | 9 +++ .../embeddables/resolver/embeddable.tsx | 34 +++++++++ .../public/embeddables/resolver/factory.ts | 31 ++++++++ .../public/embeddables/resolver/index.ts | 8 +++ x-pack/plugins/endpoint/public/index.ts | 21 ++++++ x-pack/plugins/endpoint/public/plugin.ts | 38 ++++++++++ x-pack/plugins/endpoint/server/index.ts | 26 +++++++ x-pack/plugins/endpoint/server/plugin.ts | 30 ++++++++ .../plugins/endpoint/server/routes/index.ts | 24 +++++++ x-pack/scripts/functional_tests.js | 1 + .../api_integration/apis/endpoint/index.ts | 13 ++++ .../api_integration/apis/endpoint/resolver.ts | 29 ++++++++ x-pack/test/api_integration/apis/index.js | 1 + x-pack/test/api_integration/config.js | 1 + x-pack/test/plugin_functional/config.ts | 72 +++++++++++++++++++ .../ftr_provider_context.d.ts | 11 +++ x-pack/test/plugin_functional/page_objects.ts | 6 ++ .../plugins/resolver_test/kibana.json | 9 +++ .../applications/resolver_test/index.tsx | 63 ++++++++++++++++ .../plugins/resolver_test/public/index.ts | 10 +++ .../plugins/resolver_test/public/plugin.ts | 53 ++++++++++++++ x-pack/test/plugin_functional/services.ts | 7 ++ .../test_suites/resolver/index.ts | 27 +++++++ 24 files changed, 529 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/endpoint/kibana.json create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts create mode 100644 x-pack/plugins/endpoint/public/embeddables/resolver/index.ts create mode 100644 x-pack/plugins/endpoint/public/index.ts create mode 100644 x-pack/plugins/endpoint/public/plugin.ts create mode 100644 x-pack/plugins/endpoint/server/index.ts create mode 100644 x-pack/plugins/endpoint/server/plugin.ts create mode 100644 x-pack/plugins/endpoint/server/routes/index.ts create mode 100644 x-pack/test/api_integration/apis/endpoint/index.ts create mode 100644 x-pack/test/api_integration/apis/endpoint/resolver.ts create mode 100644 x-pack/test/plugin_functional/config.ts create mode 100644 x-pack/test/plugin_functional/ftr_provider_context.d.ts create mode 100644 x-pack/test/plugin_functional/page_objects.ts create mode 100644 x-pack/test/plugin_functional/plugins/resolver_test/kibana.json create mode 100644 x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx create mode 100644 x-pack/test/plugin_functional/plugins/resolver_test/public/index.ts create mode 100644 x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts create mode 100644 x-pack/test/plugin_functional/services.ts create mode 100644 x-pack/test/plugin_functional/test_suites/resolver/index.ts diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 6d0da2f0b693d..180aafe504c63 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -9,6 +9,7 @@ "xpack.canvas": "legacy/plugins/canvas", "xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication", "xpack.dashboardMode": "legacy/plugins/dashboard_mode", + "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "legacy/plugins/file_upload", "xpack.graph": "legacy/plugins/graph", @@ -18,20 +19,20 @@ "xpack.infra": "legacy/plugins/infra", "xpack.kueryAutocomplete": "legacy/plugins/kuery_autocomplete", "xpack.lens": "legacy/plugins/lens", - "xpack.licensing": "plugins/licensing", "xpack.licenseMgmt": "legacy/plugins/license_management", - "xpack.maps": "legacy/plugins/maps", - "xpack.ml": "legacy/plugins/ml", + "xpack.licensing": "plugins/licensing", "xpack.logstash": "legacy/plugins/logstash", "xpack.main": "legacy/plugins/xpack_main", + "xpack.maps": "legacy/plugins/maps", + "xpack.ml": "legacy/plugins/ml", "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "legacy/plugins/remote_clusters", "xpack.reporting": [ "plugins/reporting", "legacy/plugins/reporting" ], "xpack.rollupJobs": "legacy/plugins/rollup", "xpack.searchProfiler": "legacy/plugins/searchprofiler", - "xpack.siem": "legacy/plugins/siem", "xpack.security": ["legacy/plugins/security", "plugins/security"], "xpack.server": "legacy/server", + "xpack.siem": "legacy/plugins/siem", "xpack.snapshotRestore": "legacy/plugins/snapshot_restore", "xpack.spaces": ["legacy/plugins/spaces", "plugins/spaces"], "xpack.taskManager": "legacy/plugins/task_manager", diff --git a/x-pack/plugins/endpoint/kibana.json b/x-pack/plugins/endpoint/kibana.json new file mode 100644 index 0000000000000..a7fd20b93f62d --- /dev/null +++ b/x-pack/plugins/endpoint/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "endpoint", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "endpoint"], + "requiredPlugins": ["embeddable"], + "server": true, + "ui": true +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx new file mode 100644 index 0000000000000..55f9fd52f4662 --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/embeddable.tsx @@ -0,0 +1,34 @@ +/* + * 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 { + EmbeddableInput, + IContainer, + Embeddable, +} from '../../../../../../src/plugins/embeddable/public'; + +export class ResolverEmbeddable extends Embeddable { + public readonly type = 'resolver'; + constructor(initialInput: EmbeddableInput, parent?: IContainer) { + super( + // Input state is irrelevant to this embeddable, just pass it along. + initialInput, + // Initial output state - this embeddable does not do anything with output, so just + // pass along an empty object. + {}, + // Optional parent component, this embeddable can optionally be rendered inside a container. + parent + ); + } + + public render(node: HTMLElement) { + node.innerHTML = '
Welcome from Resolver
'; + } + + public reload(): void { + throw new Error('Method not implemented.'); + } +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts new file mode 100644 index 0000000000000..aef2e309254ef --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/factory.ts @@ -0,0 +1,31 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ResolverEmbeddable } from './'; +import { + EmbeddableFactory, + EmbeddableInput, + IContainer, +} from '../../../../../../src/plugins/embeddable/public'; + +export class ResolverEmbeddableFactory extends EmbeddableFactory { + public readonly type = 'resolver'; + + public isEditable() { + return true; + } + + public async create(initialInput: EmbeddableInput, parent?: IContainer) { + return new ResolverEmbeddable(initialInput, parent); + } + + public getDisplayName() { + return i18n.translate('xpack.endpoint.resolver.displayNameTitle', { + defaultMessage: 'Resolver', + }); + } +} diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/index.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/index.ts new file mode 100644 index 0000000000000..e4f3cc90ae30a --- /dev/null +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { ResolverEmbeddableFactory } from './factory'; +export { ResolverEmbeddable } from './embeddable'; diff --git a/x-pack/plugins/endpoint/public/index.ts b/x-pack/plugins/endpoint/public/index.ts new file mode 100644 index 0000000000000..e6a7683efd9a3 --- /dev/null +++ b/x-pack/plugins/endpoint/public/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { PluginInitializer } from 'kibana/public'; +import { + EndpointPlugin, + EndpointPluginStart, + EndpointPluginSetup, + EndpointPluginStartDependencies, + EndpointPluginSetupDependencies, +} from './plugin'; + +export const plugin: PluginInitializer< + EndpointPluginSetup, + EndpointPluginStart, + EndpointPluginSetupDependencies, + EndpointPluginStartDependencies +> = () => new EndpointPlugin(); diff --git a/x-pack/plugins/endpoint/public/plugin.ts b/x-pack/plugins/endpoint/public/plugin.ts new file mode 100644 index 0000000000000..21bf1b3cdea12 --- /dev/null +++ b/x-pack/plugins/endpoint/public/plugin.ts @@ -0,0 +1,38 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/public'; +import { IEmbeddableSetup } from 'src/plugins/embeddable/public'; +import { ResolverEmbeddableFactory } from './embeddables/resolver'; + +export type EndpointPluginStart = void; +export type EndpointPluginSetup = void; +export interface EndpointPluginSetupDependencies { + embeddable: IEmbeddableSetup; +} + +export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface + +export class EndpointPlugin + implements + Plugin< + EndpointPluginSetup, + EndpointPluginStart, + EndpointPluginSetupDependencies, + EndpointPluginStartDependencies + > { + public setup(_core: CoreSetup, plugins: EndpointPluginSetupDependencies) { + const resolverEmbeddableFactory = new ResolverEmbeddableFactory(); + plugins.embeddable.registerEmbeddableFactory( + resolverEmbeddableFactory.type, + resolverEmbeddableFactory + ); + } + + public start() {} + + public stop() {} +} diff --git a/x-pack/plugins/endpoint/server/index.ts b/x-pack/plugins/endpoint/server/index.ts new file mode 100644 index 0000000000000..f10bc7ee51b2c --- /dev/null +++ b/x-pack/plugins/endpoint/server/index.ts @@ -0,0 +1,26 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { PluginInitializer } from 'src/core/server'; +import { + EndpointPlugin, + EndpointPluginStart, + EndpointPluginSetup, + EndpointPluginStartDependencies, + EndpointPluginSetupDependencies, +} from './plugin'; + +export const config = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), +}; + +export const plugin: PluginInitializer< + EndpointPluginStart, + EndpointPluginSetup, + EndpointPluginStartDependencies, + EndpointPluginSetupDependencies +> = () => new EndpointPlugin(); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts new file mode 100644 index 0000000000000..400b906c5230e --- /dev/null +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -0,0 +1,30 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/server'; +import { addRoutes } from './routes'; + +export type EndpointPluginStart = void; +export type EndpointPluginSetup = void; +export interface EndpointPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface + +export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface + +export class EndpointPlugin + implements + Plugin< + EndpointPluginStart, + EndpointPluginSetup, + EndpointPluginStartDependencies, + EndpointPluginSetupDependencies + > { + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + addRoutes(router); + } + + public start() {} +} diff --git a/x-pack/plugins/endpoint/server/routes/index.ts b/x-pack/plugins/endpoint/server/routes/index.ts new file mode 100644 index 0000000000000..517ee2a853660 --- /dev/null +++ b/x-pack/plugins/endpoint/server/routes/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { IRouter } from 'kibana/server'; + +export function addRoutes(router: IRouter) { + router.get( + { + path: '/api/endpoint/hello-world', + validate: false, + }, + async function greetingIndex(_context, _request, response) { + return response.ok({ + body: { hello: 'world' }, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + ); +} diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 2ac8fff6ef8ab..18ab9bad52450 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -15,6 +15,7 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/plugin_api_integration/config.js'), + require.resolve('../test/plugin_functional/config'), require.resolve('../test/kerberos_api_integration/config'), require.resolve('../test/kerberos_api_integration/anonymous_access.config'), require.resolve('../test/saml_api_integration/config'), diff --git a/x-pack/test/api_integration/apis/endpoint/index.ts b/x-pack/test/api_integration/apis/endpoint/index.ts new file mode 100644 index 0000000000000..e0ffbb13e5978 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function endpointAPIIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('Endpoint plugin', function() { + loadTestFile(require.resolve('./resolver')); + }); +} diff --git a/x-pack/test/api_integration/apis/endpoint/resolver.ts b/x-pack/test/api_integration/apis/endpoint/resolver.ts new file mode 100644 index 0000000000000..96d16e0d76e40 --- /dev/null +++ b/x-pack/test/api_integration/apis/endpoint/resolver.ts @@ -0,0 +1,29 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const commonHeaders = { + Accept: 'application/json', + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + describe('Resolver api', function() { + it('should respond to hello-world', async function() { + const { body } = await supertest + .get('/api/endpoint/hello-world') + .set(commonHeaders) + .expect('Content-Type', /application\/json/) + .expect(200); + + expect(body).to.eql({ hello: 'world' }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index ca339e9f407f2..ddf2c9a13ff67 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -28,5 +28,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./short_urls')); loadTestFile(require.resolve('./lens')); loadTestFile(require.resolve('./licensing')); + loadTestFile(require.resolve('./endpoint')); }); } diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 9c67dfe61b957..e5860fba80770 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -23,6 +23,7 @@ export async function getApiIntegrationConfig({ readConfigFile }) { ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), '--xpack.security.session.idleTimeout=3600000', // 1 hour '--optimize.enabled=false', + '--xpack.endpoint.enabled=true', ], }, esTestCluster: { diff --git a/x-pack/test/plugin_functional/config.ts b/x-pack/test/plugin_functional/config.ts new file mode 100644 index 0000000000000..6c3c496da71f6 --- /dev/null +++ b/x-pack/test/plugin_functional/config.ts @@ -0,0 +1,72 @@ +/* + * 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 { resolve } from 'path'; +import fs from 'fs'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { services } from './services'; +import { pageObjects } from './page_objects'; + +// the default export of config files must be a config provider +// that returns an object with the projects config values + +/* eslint-disable import/no-default-export */ +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const xpackFunctionalConfig = await readConfigFile(require.resolve('../functional/config.js')); + + // Find all folders in ./plugins since we treat all them as plugin folder + const allFiles = fs.readdirSync(resolve(__dirname, 'plugins')); + const plugins = allFiles.filter(file => + fs.statSync(resolve(__dirname, 'plugins', file)).isDirectory() + ); + + return { + // list paths to the files that contain your plugins tests + testFiles: [resolve(__dirname, './test_suites/resolver')], + + services, + pageObjects, + + servers: xpackFunctionalConfig.get('servers'), + + esTestCluster: xpackFunctionalConfig.get('esTestCluster'), + + kbnTestServer: { + ...xpackFunctionalConfig.get('kbnTestServer'), + serverArgs: [ + ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + ...plugins.map(pluginDir => `--plugin-path=${resolve(__dirname, 'plugins', pluginDir)}`), + // Required to load new platform plugins via `--plugin-path` flag. + '--env.name=development', + '--xpack.endpoint.enabled=true', + ], + }, + uiSettings: xpackFunctionalConfig.get('uiSettings'), + // the apps section defines the urls that + // `PageObjects.common.navigateTo(appKey)` will use. + // Merge urls for your plugin with the urls defined in + // Kibana's config in order to use this helper + apps: { + ...xpackFunctionalConfig.get('apps'), + resolverTest: { + pathname: '/app/resolver_test', + }, + }, + + // choose where esArchiver should load archives from + esArchiver: { + directory: resolve(__dirname, 'es_archives'), + }, + + // choose where screenshots should be saved + screenshots: { + directory: resolve(__dirname, 'screenshots'), + }, + + junit: { + reportName: 'Chrome X-Pack UI Plugin Functional Tests', + }, + }; +} diff --git a/x-pack/test/plugin_functional/ftr_provider_context.d.ts b/x-pack/test/plugin_functional/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..271f313d4bda9 --- /dev/null +++ b/x-pack/test/plugin_functional/ftr_provider_context.d.ts @@ -0,0 +1,11 @@ +/* + * 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { services } from './services'; +import { pageObjects } from './page_objects'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/plugin_functional/page_objects.ts b/x-pack/test/plugin_functional/page_objects.ts new file mode 100644 index 0000000000000..a216b0f2cd47a --- /dev/null +++ b/x-pack/test/plugin_functional/page_objects.ts @@ -0,0 +1,6 @@ +/* + * 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 { pageObjects } from '../functional/page_objects'; diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json new file mode 100644 index 0000000000000..c715a0aaa3b20 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/resolver_test/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "resolver_test", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack", "resolver_test"], + "requiredPlugins": ["embeddable"], + "server": false, + "ui": true +} diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx new file mode 100644 index 0000000000000..98baad6a18411 --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -0,0 +1,63 @@ +/* + * 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 * as React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters } from 'kibana/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { IEmbeddable } from 'src/plugins/embeddable/public'; +import { useEffect } from 'react'; + +/** + * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. + */ +export function renderApp( + { element }: AppMountParameters, + embeddable: Promise +) { + ReactDOM.render( + + + , + element + ); + + return () => { + ReactDOM.unmountComponentAtNode(element); + }; +} + +const AppRoot = React.memo( + ({ embeddable: embeddablePromise }: { embeddable: Promise }) => { + const [embeddable, setEmbeddable] = React.useState(undefined); + const [renderTarget, setRenderTarget] = React.useState(null); + + useEffect(() => { + let cleanUp; + Promise.race([ + new Promise((_resolve, reject) => { + cleanUp = reject; + }), + embeddablePromise, + ]).then(value => { + setEmbeddable(value); + }); + + return cleanUp; + }, [embeddablePromise]); + + useEffect(() => { + if (embeddable && renderTarget) { + embeddable.render(renderTarget); + return () => { + embeddable.destroy(); + }; + } + }, [embeddable, renderTarget]); + + return
; + } +); diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/index.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/index.ts new file mode 100644 index 0000000000000..c5f3c0e19138f --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { PluginInitializer } from 'kibana/public'; +import { ResolverTestPlugin } from './plugin'; + +export const plugin: PluginInitializer = () => new ResolverTestPlugin(); diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts new file mode 100644 index 0000000000000..f063271f4b5dd --- /dev/null +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/plugin.ts @@ -0,0 +1,53 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { IEmbeddable, IEmbeddableStart } from '../../../../../../src/plugins/embeddable/public'; + +export type ResolverTestPluginSetup = void; +export type ResolverTestPluginStart = void; +export interface ResolverTestPluginSetupDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface ResolverTestPluginStartDependencies { + embeddable: IEmbeddableStart; +} + +export class ResolverTestPlugin + implements + Plugin< + ResolverTestPluginSetup, + ResolverTestPluginStart, + ResolverTestPluginSetupDependencies, + ResolverTestPluginStartDependencies + > { + private resolveEmbeddable!: ( + value: IEmbeddable | undefined | PromiseLike | undefined + ) => void; + private embeddablePromise: Promise = new Promise< + IEmbeddable | undefined + >(resolve => { + this.resolveEmbeddable = resolve; + }); + public setup(core: CoreSetup) { + core.application.register({ + id: 'resolver_test', + title: i18n.translate('xpack.resolver_test.pluginTitle', { + defaultMessage: 'Resolver Test', + }), + mount: async (_context, params) => { + const { renderApp } = await import('./applications/resolver_test'); + return renderApp(params, this.embeddablePromise); + }, + }); + } + + public start(...args: [unknown, { embeddable: IEmbeddableStart }]) { + const [, plugins] = args; + const factory = plugins.embeddable.getEmbeddableFactory('resolver'); + this.resolveEmbeddable(factory.create({ id: 'test basic render' })); + } + public stop() {} +} diff --git a/x-pack/test/plugin_functional/services.ts b/x-pack/test/plugin_functional/services.ts new file mode 100644 index 0000000000000..5c807720b2867 --- /dev/null +++ b/x-pack/test/plugin_functional/services.ts @@ -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 { services } from '../functional/services'; diff --git a/x-pack/test/plugin_functional/test_suites/resolver/index.ts b/x-pack/test/plugin_functional/test_suites/resolver/index.ts new file mode 100644 index 0000000000000..a0735f216e309 --- /dev/null +++ b/x-pack/test/plugin_functional/test_suites/resolver/index.ts @@ -0,0 +1,27 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + + describe('Resolver embeddable test app', function() { + this.tags('ciGroup7'); + + beforeEach(async function() { + await pageObjects.common.navigateToApp('resolverTest'); + }); + + it('renders a container div for the embeddable', async function() { + await testSubjects.existOrFail('resolverEmbeddableContainer'); + }); + it('renders resolver', async function() { + await testSubjects.existOrFail('resolverEmbeddable'); + }); + }); +} From f7f00819dc9d29b905107660173530e938ef901b Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 6 Dec 2019 16:17:18 -0500 Subject: [PATCH 11/26] Update default path linked on Kibana sidebar to avoid basename warning in browser. (#52008) --- x-pack/legacy/plugins/uptime/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/uptime/index.ts b/x-pack/legacy/plugins/uptime/index.ts index 3cd0ffb1a2942..c8de623cb0a13 100644 --- a/x-pack/legacy/plugins/uptime/index.ts +++ b/x-pack/legacy/plugins/uptime/index.ts @@ -29,7 +29,7 @@ export const uptime = (kibana: any) => }), main: 'plugins/uptime/app', order: 8900, - url: '/app/uptime/', + url: '/app/uptime#/', }, home: ['plugins/uptime/register_feature'], }, From df21ec3fcfcf11bec871b3f740d2830a74a2b9b5 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Fri, 6 Dec 2019 22:24:03 +0100 Subject: [PATCH 12/26] Deprecate recompose part 1 (#50806) --- .../link_to/redirect_to_node_logs.test.tsx | 60 +- .../public/components/and_or_badge/index.tsx | 3 +- .../arrows/__snapshots__/index.test.tsx.snap | 15 +- .../public/components/arrows/index.test.tsx | 6 +- .../siem/public/components/arrows/index.tsx | 3 +- .../certificate_fingerprint/index.tsx | 3 +- .../public/components/charts/barchart.tsx | 3 +- .../__snapshots__/utility_bar.test.tsx.snap | 54 +- .../utility_bar_action.test.tsx.snap | 12 +- .../utility_bar_group.test.tsx.snap | 12 +- .../utility_bar_section.test.tsx.snap | 16 +- .../utility_bar_text.test.tsx.snap | 8 +- .../utility_bar/utility_bar.test.tsx | 2 +- .../utility_bar/utility_bar.tsx | 3 +- .../utility_bar/utility_bar_action.test.tsx | 2 +- .../utility_bar/utility_bar_action.tsx | 2 + .../utility_bar/utility_bar_group.test.tsx | 2 +- .../utility_bar/utility_bar_group.tsx | 1 + .../utility_bar/utility_bar_section.test.tsx | 2 +- .../utility_bar/utility_bar_section.tsx | 1 + .../utility_bar/utility_bar_text.test.tsx | 2 +- .../utility_bar/utility_bar_text.tsx | 1 + .../public/components/direction/index.tsx | 3 +- .../drag_drop_context_wrapper.test.tsx.snap | 819 +++--- .../draggable_wrapper.test.tsx.snap | 457 +--- .../droppable_wrapper.test.tsx.snap | 431 +--- .../drag_drop_context_wrapper.test.tsx | 2 +- .../drag_drop_context_wrapper.tsx | 2 + .../drag_and_drop/draggable_wrapper.test.tsx | 2 +- .../drag_and_drop/draggable_wrapper.tsx | 2 + .../drag_and_drop/droppable_wrapper.test.tsx | 2 +- .../drag_and_drop/droppable_wrapper.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 47 +- .../draggables/field_badge/index.tsx | 4 +- .../components/draggables/index.test.tsx | 48 +- .../public/components/draggables/index.tsx | 7 +- .../siem/public/components/duration/index.tsx | 3 +- .../__snapshots__/embeddable.test.tsx.snap | 12 +- .../embeddable_header.test.tsx.snap | 23 +- .../embeddables/embeddable.test.tsx | 9 +- .../embeddables/embeddable_header.test.tsx | 6 +- .../point_tool_tip_content.test.tsx.snap | 26 +- .../point_tool_tip_content.test.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 22 +- .../public/components/empty_page/index.tsx | 3 +- .../__snapshots__/event_details.test.tsx.snap | 2190 ++++++++++++----- .../__snapshots__/json_view.test.tsx.snap | 192 +- .../event_details/event_details.test.tsx | 24 +- .../components/event_details/json_view.tsx | 3 +- .../components/external_link_icon/index.tsx | 3 +- .../field_renderers.test.tsx.snap | 414 +--- .../field_renderers/field_renderers.test.tsx | 26 +- .../field_renderers/field_renderers.tsx | 5 +- .../components/fields_browser/category.tsx | 3 +- .../fields_browser/category_title.tsx | 47 +- .../components/fields_browser/fields_pane.tsx | 3 +- .../components/fields_browser/header.tsx | 7 +- .../filters_global.test.tsx.snap | 14 +- .../filters_global/filters_global.test.tsx | 3 +- .../filters_global/filters_global.tsx | 3 +- .../flow_direction_select.test.tsx.snap | 29 +- .../flow_target_select.test.tsx.snap | 34 +- .../flow_controls/flow_direction_select.tsx | 3 +- .../flow_controls/flow_target_select.tsx | 3 +- .../flyout/__snapshots__/index.test.tsx.snap | 22 +- .../public/components/flyout/index.test.tsx | 2 +- .../siem/public/components/flyout/index.tsx | 2 + .../pane/__snapshots__/index.test.tsx.snap | 34 +- .../components/flyout/pane/index.test.tsx | 2 +- .../public/components/flyout/pane/index.tsx | 2 + .../components/formatted_bytes/index.test.tsx | 4 + .../__snapshots__/index.test.tsx.snap | 6 +- .../components/formatted_date/index.test.tsx | 3 +- .../components/formatted_date/index.tsx | 5 +- .../components/formatted_duration/index.tsx | 3 +- .../formatted_duration/tooltip/index.tsx | 5 +- .../public/components/formatted_ip/index.tsx | 7 +- .../__snapshots__/index.test.tsx.snap | 106 +- .../components/header_global/index.test.tsx | 7 +- .../__snapshots__/index.test.tsx.snap | 58 +- .../components/header_page/index.test.tsx | 20 +- .../__snapshots__/index.test.tsx.snap | 27 +- .../components/header_section/index.test.tsx | 6 +- .../public/components/help_menu/index.tsx | 5 +- .../siem/public/components/inspect/index.tsx | 3 +- .../ip/__snapshots__/index.test.tsx.snap | 4 +- .../siem/public/components/ip/index.tsx | 3 +- .../components/ja3_fingerprint/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 21 +- .../components/link_icon/index.test.tsx | 8 +- .../public/components/link_to/link_to.tsx | 3 +- .../siem/public/components/links/index.tsx | 40 +- .../loader/__snapshots__/index.test.tsx.snap | 33 +- .../siem/public/components/loader/index.tsx | 4 +- .../siem/public/components/loading/index.tsx | 3 +- .../localized_date_tooltip/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 46 +- .../siem/public/components/markdown/index.tsx | 87 +- .../entity_draggable.test.tsx.snap | 2 +- .../__snapshots__/anomaly_score.test.tsx.snap | 2 +- .../draggable_score.test.tsx.snap | 4 +- .../public/components/navigation/index.tsx | 2 + .../netflow/__snapshots__/index.test.tsx.snap | 452 ++-- .../components/netflow/fingerprints/index.tsx | 3 +- .../public/components/netflow/index.test.tsx | 5 +- .../siem/public/components/netflow/index.tsx | 3 +- .../duration_event_start_end.tsx | 3 +- .../netflow/netflow_columns/index.tsx | 3 +- .../netflow/netflow_columns/user_process.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 49 +- .../__snapshots__/new_note.test.tsx.snap | 55 +- .../components/notes/add_note/index.tsx | 5 +- .../components/notes/add_note/new_note.tsx | 3 +- .../siem/public/components/notes/columns.tsx | 3 +- .../siem/public/components/notes/helpers.tsx | 3 +- .../components/notes/note_card/index.tsx | 3 +- .../notes/note_card/note_card_body.tsx | 3 +- .../notes/note_card/note_card_header.tsx | 3 +- .../notes/note_card/note_created.tsx | 3 +- .../delete_timeline_modal.tsx | 3 +- .../components/open_timeline/index.test.tsx | 1 - .../note_previews/index.test.tsx | 16 +- .../open_timeline/note_previews/index.tsx | 6 +- .../note_previews/note_preview.tsx | 3 +- .../open_timeline/open_timeline.tsx | 3 +- .../open_timeline_modal_body.tsx | 3 +- .../open_timeline/search_row/index.tsx | 3 +- .../timelines_table/common_columns.test.tsx | 8 +- .../timelines_table/common_columns.tsx | 4 +- .../open_timeline/timelines_table/index.tsx | 3 +- .../open_timeline/title_row/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 58 +- .../index.test.tsx | 46 +- .../__snapshots__/index.test.tsx.snap | 6 +- .../histogram_signals/index.test.tsx | 2 +- .../hosts/authentications_table/index.tsx | 3 +- .../page/hosts/first_last_seen_host/index.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 342 ++- .../page/hosts/host_overview/index.test.tsx | 2 +- .../__snapshots__/index.test.tsx.snap | 314 ++- .../page/hosts/hosts_table/index.test.tsx | 2 +- .../page/hosts/hosts_table/index.tsx | 2 + .../__snapshots__/index.test.tsx.snap | 342 ++- .../uncommon_process_table/index.test.tsx | 2 +- .../hosts/uncommon_process_table/index.tsx | 5 +- .../flow_target_select_connected/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 266 +- .../page/network/ip_overview/index.test.tsx | 2 +- .../page/network/ip_overview/index.tsx | 3 +- .../is_ptr_included.test.tsx.snap | 6 +- .../network_dns_table/is_ptr_included.tsx | 3 +- .../page/overview/overview_host/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 251 +- .../overview/overview_host_stats/index.tsx | 5 +- .../page/overview/overview_network/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 155 +- .../overview/overview_network_stats/index.tsx | 5 +- .../siem/public/components/pin/index.tsx | 23 +- .../port/__snapshots__/index.test.tsx.snap | 18 +- .../public/components/port/index.test.tsx | 4 +- .../siem/public/components/port/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 32 +- .../components/progress_inline/index.test.tsx | 9 +- .../__snapshots__/index.test.tsx.snap | 21 +- .../components/resize_handle/index.test.tsx | 16 +- .../__snapshots__/index.test.tsx.snap | 17 +- .../components/skeleton_row/index.test.tsx | 6 +- .../source_destination/geo_fields.tsx | 5 +- .../components/source_destination/index.tsx | 3 +- .../source_destination/ip_with_port.tsx | 49 +- .../components/source_destination/network.tsx | 3 +- .../source_destination_arrows.tsx | 7 +- .../source_destination_with_arrows.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 306 +-- .../__snapshots__/index.test.tsx.snap | 12 +- .../public/components/subtitle/index.test.tsx | 6 +- .../__snapshots__/helpers.test.tsx.snap | 52 +- .../public/components/tables/helpers.test.tsx | 6 +- .../timeline/auto_save_warning/index.tsx | 3 +- .../body/column_headers/actions/index.tsx | 5 +- .../column_headers/events_select/helpers.tsx | 3 +- .../column_headers/events_select/index.tsx | 3 +- .../filter/__snapshots__/index.test.tsx.snap | 12 +- .../body/column_headers/filter/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 76 +- .../header_tooltip_content/index.tsx | 3 +- .../column_headers/range_picker/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 9 +- .../body/column_headers/text_filter/index.tsx | 3 +- .../__snapshots__/index.test.tsx.snap | 350 ++- .../body/data_driven_columns/index.test.tsx | 20 +- .../empty_column_renderer.test.tsx.snap | 2 +- .../formatted_field.test.tsx.snap | 5 +- .../get_column_renderer.test.tsx.snap | 2 +- .../host_working_dir.test.tsx.snap | 37 +- .../plain_column_renderer.test.tsx.snap | 2 +- .../process_draggable.test.tsx.snap | 25 +- .../user_host_working_dir.test.tsx.snap | 52 +- .../generic_details.test.tsx.snap | 218 +- .../generic_file_details.test.tsx.snap | 30 +- .../primary_secondary_user_info.test.tsx.snap | 3 +- .../renderers/auditd/generic_details.test.tsx | 18 +- .../body/renderers/auditd/generic_details.tsx | 5 +- .../auditd/generic_file_details.test.tsx | 38 +- .../renderers/auditd/generic_file_details.tsx | 5 +- .../auditd/primary_secondary_user_info.tsx | 5 +- .../auditd/session_user_host_working_dir.tsx | 3 +- .../body/renderers/formatted_field.tsx | 3 +- .../body/renderers/host_working_dir.tsx | 9 +- .../timeline/body/renderers/netflow.tsx | 8 +- .../body/renderers/process_draggable.test.tsx | 44 +- .../body/renderers/process_draggable.tsx | 5 +- .../suricata_details.test.tsx.snap | 567 +---- .../suricata_signature.test.tsx.snap | 45 +- .../suricata/suricata_details.test.tsx | 14 +- .../renderers/suricata/suricata_details.tsx | 4 +- .../body/renderers/suricata/suricata_refs.tsx | 3 +- .../suricata/suricata_signature.test.tsx | 4 +- .../renderers/suricata/suricata_signature.tsx | 7 +- .../__snapshots__/auth_ssh.test.tsx.snap | 34 +- .../generic_details.test.tsx.snap | 188 +- .../generic_file_details.test.tsx.snap | 23 +- .../__snapshots__/package.test.tsx.snap | 44 +- .../body/renderers/system/auth_ssh.test.tsx | 124 +- .../body/renderers/system/auth_ssh.tsx | 3 +- .../body/renderers/system/generic_details.tsx | 5 +- .../system/generic_file_details.test.tsx | 16 +- .../renderers/system/generic_file_details.tsx | 5 +- .../body/renderers/system/package.test.tsx | 36 +- .../body/renderers/system/package.tsx | 3 +- .../renderers/user_host_working_dir.test.tsx | 40 +- .../body/renderers/user_host_working_dir.tsx | 3 +- .../__snapshots__/zeek_details.test.tsx.snap | 2 +- .../zeek_signature.test.tsx.snap | 180 +- .../body/renderers/zeek/zeek_details.test.tsx | 24 +- .../body/renderers/zeek/zeek_details.tsx | 24 +- .../renderers/zeek/zeek_signature.test.tsx | 21 +- .../body/renderers/zeek/zeek_signature.tsx | 67 +- .../sort_indicator.test.tsx.snap | 5 +- .../timeline/body/sort/sort_indicator.tsx | 3 +- .../data_providers.test.tsx.snap | 157 +- .../__snapshots__/empty.test.tsx.snap | 39 +- .../__snapshots__/provider.test.tsx.snap | 27 +- .../__snapshots__/providers.test.tsx.snap | 654 +++-- .../timeline/data_providers/empty.test.tsx | 6 +- .../timeline/data_providers/empty.tsx | 3 +- .../timeline/data_providers/index.tsx | 3 +- .../timeline/data_providers/provider.tsx | 3 +- .../data_providers/provider_badge.tsx | 3 +- .../provider_item_and_drag_drop.tsx | 3 +- .../data_providers/providers.test.tsx | 24 +- .../timeline/data_providers/providers.tsx | 3 +- .../components/timeline/footer/index.tsx | 36 +- .../timeline/footer/last_updated.tsx | 3 +- .../timeline/properties/helpers.tsx | 178 +- .../search_or_filter/search_or_filter.tsx | 3 +- .../components/with_hover_actions/index.tsx | 13 +- .../__snapshots__/index.test.tsx.snap | 60 +- .../components/wrapper_page/index.test.tsx | 8 +- .../public/components/wrapper_page/index.tsx | 4 +- .../public/containers/ip_overview/index.tsx | 3 +- .../containers/kpi_host_details/index.tsx | 3 +- .../public/containers/kpi_hosts/index.tsx | 5 +- .../public/containers/kpi_network/index.tsx | 5 +- .../overview/overview_host/index.tsx | 5 +- .../overview/overview_network/index.tsx | 63 +- .../lib/clipboard/with_copy_to_clipboard.tsx | 3 +- .../siem/public/mock/test_providers.tsx | 5 +- .../legacy/plugins/siem/public/pages/404.tsx | 4 +- .../plugins/siem/public/pages/home/index.tsx | 10 +- .../public/pages/hosts/hosts_empty_page.tsx | 3 +- .../pages/network/network_empty_page.tsx | 3 +- .../siem/public/pages/overview/summary.tsx | 3 +- x-pack/legacy/plugins/siem/public/routes.tsx | 4 +- 274 files changed, 6526 insertions(+), 6174 deletions(-) diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx index 1916e84ef21d3..7a63406bb419a 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx @@ -34,11 +34,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct container filter', () => { @@ -47,11 +47,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct pod filter', () => { @@ -60,11 +60,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct position', () => { @@ -75,11 +75,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct user-defined filter', () => { @@ -92,11 +92,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); it('renders a redirect with the correct custom source id', () => { @@ -107,11 +107,11 @@ describe('RedirectToNodeLogs component', () => { ); expect(component).toMatchInlineSnapshot(` - -`); + + `); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx index 3548fb7c0e671..be449e3d422d9 100644 --- a/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/and_or_badge/index.tsx @@ -6,7 +6,6 @@ import { EuiBadge } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import * as i18n from './translations'; @@ -39,7 +38,7 @@ export type AndOr = 'and' | 'or'; /** Displays AND / OR in a round badge */ // Ref: https://github.com/elastic/eui/issues/1655 -export const AndOrBadge = pure<{ type: AndOr }>(({ type }) => { +export const AndOrBadge = React.memo<{ type: AndOr }>(({ type }) => { return ( {type === 'and' ? i18n.AND : i18n.OR} diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap index 408bcac756f47..7702695520790 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap @@ -1,9 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`arrows ArrowBody renders correctly against snapshot 1`] = ` - - + - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx index 8be0e7c267ec0..10d3c899562e8 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/arrows/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { mount } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -15,12 +15,12 @@ import { ArrowBody, ArrowHead } from '.'; describe('arrows', () => { describe('ArrowBody', () => { test('renders correctly against snapshot', () => { - const wrapper = shallow( + const wrapper = mount( ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('ArrowBody'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx b/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx index 6d5b464e0e886..dfc7645c564d2 100644 --- a/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/arrows/index.tsx @@ -6,7 +6,6 @@ import { EuiIcon } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; /** Renders the body (non-pointy part) of an arrow */ @@ -21,7 +20,7 @@ ArrowBody.displayName = 'ArrowBody'; export type ArrowDirection = 'arrowLeft' | 'arrowRight'; /** Renders the head of an arrow */ -export const ArrowHead = pure<{ +export const ArrowHead = React.memo<{ direction: ArrowDirection; }>(({ direction }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx index 37ec256ccd8c0..f8db7d754aab1 100644 --- a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.tsx @@ -6,7 +6,6 @@ import { EuiText } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DraggableBadge } from '../draggables'; @@ -36,7 +35,7 @@ FingerprintLabel.displayName = 'FingerprintLabel'; * 'tls.client_certificate.fingerprint.sha1' * 'tls.server_certificate.fingerprint.sha1' */ -export const CertificateFingerprint = pure<{ +export const CertificateFingerprint = React.memo<{ eventId: string; certificateType: CertificateType; contextId: string; diff --git a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx index 7218d7a497f19..99ad995e48852 100644 --- a/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx +++ b/x-pack/legacy/plugins/siem/public/components/charts/barchart.tsx @@ -5,7 +5,6 @@ */ import React from 'react'; - import { Chart, BarSeries, @@ -63,6 +62,7 @@ export const BarChartBaseComponent = ({ ...chartDefaultSettings, ...get('configs.settings', chartConfigs), }; + return chartConfigs.width && chartConfigs.height ? ( @@ -116,6 +116,7 @@ export const BarChartComponent = ({ }) => { const customHeight = get('customHeight', configs); const customWidth = get('customWidth', configs); + return checkIfAnyValidSeriesExist(barChart) ? ( {({ measureRef, content: { height, width } }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap index 03a04983f9f86..f082dc4023e7a 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar.test.tsx.snap @@ -1,32 +1,30 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBar it renders 1`] = ` - - - - - - Test text - - - - - Test action - - - - - - - Test action - - - - - + + + + + Test text + + + + + Test action + + + + + + + Test action + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap index 470b40cd1d960..eb20ac217b300 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap @@ -1,11 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarAction it renders 1`] = ` - - - Test action - - + + Test action + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap index 62ff1b17dd55f..8ef7ee1cfe842 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap @@ -1,11 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarGroup it renders 1`] = ` - - - - Test text - - - + + + Test text + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap index f81717c892755..2fe3b8ac5c7aa 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap @@ -1,13 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarSection it renders 1`] = ` - - - - - Test text - - - - + + + + Test text + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap index 446b5556945d8..cf635ffa49c4c 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UtilityBarText it renders 1`] = ` - - - Test text - - + + Test text + `; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx index 27688ec24530e..68522377bd847 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.test.tsx @@ -47,7 +47,7 @@ describe('UtilityBar', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBar'))).toMatchSnapshot(); }); test('it applies border styles when border is true', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx index f226e0e055391..524769361ea9d 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar.tsx @@ -8,11 +8,12 @@ import React from 'react'; import { Bar, BarProps } from './styles'; -export interface UtilityBarProps extends BarProps { +interface UtilityBarProps extends BarProps { children: React.ReactNode; } export const UtilityBar = React.memo(({ border, children }) => ( {children} )); + UtilityBar.displayName = 'UtilityBar'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx index f71bdfda705d0..7921c1ef42200 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.test.tsx @@ -22,7 +22,7 @@ describe('UtilityBarAction', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarAction'))).toMatchSnapshot(); }); test('it renders a popover', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx index 2ad48bc9b9c92..f695c33a37447 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_action.tsx @@ -37,6 +37,7 @@ const Popover = React.memo( ); } ); + Popover.displayName = 'Popover'; export interface UtilityBarActionProps extends LinkIconProps { @@ -71,4 +72,5 @@ export const UtilityBarAction = React.memo( ) ); + UtilityBarAction.displayName = 'UtilityBarAction'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx index 84ad96c5a1e5e..294d27fa95b3d 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.test.tsx @@ -24,6 +24,6 @@ describe('UtilityBarGroup', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarGroup'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx index 1e23fd3498199..723035df672a9 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_group.tsx @@ -15,4 +15,5 @@ export interface UtilityBarGroupProps { export const UtilityBarGroup = React.memo(({ children }) => ( {children} )); + UtilityBarGroup.displayName = 'UtilityBarGroup'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx index 2dfc1d3b8d193..e0e0acc3a71c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.test.tsx @@ -26,6 +26,6 @@ describe('UtilityBarSection', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarSection'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx index c457e6bc3dee0..42532c0355607 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_section.tsx @@ -15,4 +15,5 @@ export interface UtilityBarSectionProps { export const UtilityBarSection = React.memo(({ children }) => ( {children} )); + UtilityBarSection.displayName = 'UtilityBarSection'; diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx index 0743e5cab02b4..29e1844bb2d4f 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.test.tsx @@ -22,6 +22,6 @@ describe('UtilityBarText', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('UtilityBarText'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx index f8eb25f03d4ad..6195e008dbe27 100644 --- a/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx +++ b/x-pack/legacy/plugins/siem/public/components/detection_engine/utility_bar/utility_bar_text.tsx @@ -15,4 +15,5 @@ export interface UtilityBarTextProps { export const UtilityBarText = React.memo(({ children }) => ( {children} )); + UtilityBarText.displayName = 'UtilityBarText'; diff --git a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx b/x-pack/legacy/plugins/siem/public/components/direction/index.tsx index b5d6fcfc6cef7..9295e055f918d 100644 --- a/x-pack/legacy/plugins/siem/public/components/direction/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/direction/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { NetworkDirectionEcs } from '../../graphql/types'; import { DraggableBadge } from '../draggables'; @@ -56,7 +55,7 @@ export const getDirectionIcon = ( /** * Renders a badge containing the value of `network.direction` */ -export const DirectionBadge = pure<{ +export const DirectionBadge = React.memo<{ contextId: string; direction?: string | null; eventId: string; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap index 22c7b62711795..666a8249c27d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap @@ -1,426 +1,419 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DragDropContextWrapper rendering it renders against the snapshot 1`] = ` - - - - Drag drop context wrapper children - - - + "client.bytes": Object { + "aggregatable": true, + "category": "client", + "description": "Bytes sent from the client to the server.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.bytes", + "searchable": true, + "type": "number", + }, + "client.domain": Object { + "aggregatable": true, + "category": "client", + "description": "Client domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.domain", + "searchable": true, + "type": "string", + }, + "client.geo.country_iso_code": Object { + "aggregatable": true, + "category": "client", + "description": "Country ISO code.", + "example": "CA", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "client.geo.country_iso_code", + "searchable": true, + "type": "string", + }, + }, + }, + "cloud": Object { + "fields": Object { + "cloud.account.id": Object { + "aggregatable": true, + "category": "cloud", + "description": "The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.", + "example": "666777888999", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.account.id", + "searchable": true, + "type": "string", + }, + "cloud.availability_zone": Object { + "aggregatable": true, + "category": "cloud", + "description": "Availability zone in which this host is running.", + "example": "us-east-1c", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "cloud.availability_zone", + "searchable": true, + "type": "string", + }, + }, + }, + "container": Object { + "fields": Object { + "container.id": Object { + "aggregatable": true, + "category": "container", + "description": "Unique container id.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.id", + "searchable": true, + "type": "string", + }, + "container.image.name": Object { + "aggregatable": true, + "category": "container", + "description": "Name of the image the container was built on.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.name", + "searchable": true, + "type": "string", + }, + "container.image.tag": Object { + "aggregatable": true, + "category": "container", + "description": "Container image tag.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "container.image.tag", + "searchable": true, + "type": "string", + }, + }, + }, + "destination": Object { + "fields": Object { + "destination.address": Object { + "aggregatable": true, + "category": "destination", + "description": "Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the \`.address\` field. Then it should be duplicated to \`.ip\` or \`.domain\`, depending on which one it is.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.address", + "searchable": true, + "type": "string", + }, + "destination.bytes": Object { + "aggregatable": true, + "category": "destination", + "description": "Bytes sent from the destination to the source.", + "example": "184", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.bytes", + "searchable": true, + "type": "number", + }, + "destination.domain": Object { + "aggregatable": true, + "category": "destination", + "description": "Destination domain.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.domain", + "searchable": true, + "type": "string", + }, + "destination.ip": Object { + "aggregatable": true, + "category": "destination", + "description": "IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.ip", + "searchable": true, + "type": "ip", + }, + "destination.port": Object { + "aggregatable": true, + "category": "destination", + "description": "Port of the destination.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "destination.port", + "searchable": true, + "type": "long", + }, + }, + }, + "event": Object { + "fields": Object { + "event.end": Object { + "aggregatable": true, + "category": "event", + "description": "event.end contains the date when the event ended or when the activity was last observed.", + "example": null, + "format": "", + "indexes": Array [ + "auditbeat-*", + "endgame-*", + "filebeat-*", + "packetbeat-*", + "winlogbeat-*", + ], + "name": "event.end", + "searchable": true, + "type": "date", + }, + }, + }, + "source": Object { + "fields": Object { + "source.ip": Object { + "aggregatable": true, + "category": "source", + "description": "IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.ip", + "searchable": true, + "type": "ip", + }, + "source.port": Object { + "aggregatable": true, + "category": "source", + "description": "Port of the source.", + "example": "", + "format": "", + "indexes": Array [ + "auditbeat", + "filebeat", + "packetbeat", + ], + "name": "source.port", + "searchable": true, + "type": "long", + }, + }, + }, + } + } +> + Drag drop context wrapper children + `; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap index a240d5122ac9c..aa8214938c2b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap @@ -1,443 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DraggableWrapper rendering it renders against the snapshot 1`] = ` - - - - - - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap index 23a540f0ce3b3..7c6e321395fa5 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap @@ -1,430 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DroppableWrapper rendering it renders against the snapshot 1`] = ` - - - - - draggable wrapper content - - - - + + draggable wrapper content + `; diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx index b8fba6fe2f6d8..1a8af9d99193a 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx @@ -28,7 +28,7 @@ describe('DragDropContextWrapper', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DragDropContextWrapper'))).toMatchSnapshot(); }); test('it renders the children', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx index a3528158a0317..f9e6bfcf7c236 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -114,6 +114,8 @@ const mapStateToProps = (state: State) => { export const DragDropContextWrapper = connect(mapStateToProps)(DragDropContextWrapperComponent); +DragDropContextWrapper.displayName = 'DragDropContextWrapper'; + const onBeforeCapture = (before: BeforeCapture) => { const x = window.pageXOffset !== undefined diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index d9b78836b450e..008ece5c7e69c 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -30,7 +30,7 @@ describe('DraggableWrapper', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DraggableWrapper'))).toMatchSnapshot(); }); test('it renders the children passed to the render prop', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx index c314785511201..809c46f7b53bb 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx @@ -275,6 +275,8 @@ export const DraggableWrapper = connect(null, { unRegisterProvider: dragAndDropActions.unRegisterProvider, })(DraggableWrapperComponent); +DraggableWrapper.displayName = 'DraggableWrapper'; + /** * Conditionally wraps children in an EuiPortal to ensure drag offsets are correct when dragging * from containers that have css transforms diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx index 859b30d2164dd..39abbdd4d4e38 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx @@ -30,7 +30,7 @@ describe('DroppableWrapper', () => { ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DroppableWrapper'))).toMatchSnapshot(); }); test('it renders the children when a render prop is not provided', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx index 3f789a39832f1..2b013a665af16 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx @@ -7,7 +7,6 @@ import { rgba } from 'polished'; import * as React from 'react'; import { Droppable } from 'react-beautiful-dnd'; -import { pure } from 'recompose'; import styled from 'styled-components'; interface Props { @@ -87,7 +86,7 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string `; ReactDndDropTarget.displayName = 'ReactDndDropTarget'; -export const DroppableWrapper = pure( +export const DroppableWrapper = React.memo( ({ children = null, droppableId, diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap index 1e9e89ad66641..63ba13306ecd8 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap @@ -1,29 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`draggables rendering it renders the default Badge 1`] = ` - - - A child of this - - + + + A child of this + + + `; exports[`draggables rendering it renders the default DefaultDraggable 1`] = ` - - - A child of this - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx index 5bff59494b9ad..90d8ad463b476 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/field_badge/index.tsx @@ -6,7 +6,6 @@ import { rgba } from 'polished'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; const Field = styled.div` @@ -28,11 +27,12 @@ Field.displayName = 'Field'; // Passing the styles directly to the component because the width is // being calculated and is recommended by Styled Components for performance // https://github.com/styled-components/styled-components/issues/134#issuecomment-312415291 -export const DraggableFieldBadge = pure<{ fieldId: string; fieldWidth?: string }>( +export const DraggableFieldBadge = React.memo<{ fieldId: string; fieldWidth?: string }>( ({ fieldId, fieldWidth }) => ( {fieldId} ) ); + DraggableFieldBadge.displayName = 'DraggableFieldBadge'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx index fb49329ba1501..d3dcba9526bdd 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx @@ -108,19 +108,15 @@ describe('draggables', () => { }); test('it returns null if value is undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if value is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); @@ -218,31 +214,27 @@ describe('draggables', () => { }); test('it returns null if value is undefined', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); test('it returns null if value is null', () => { - const wrapper = mountWithIntl( - - - + const wrapper = shallow( + ); expect(wrapper.isEmptyRender()).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx index 2f91cdc43b797..5b219dad9c841 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.tsx @@ -6,7 +6,6 @@ import { EuiBadge, EuiBadgeProps, EuiToolTip, IconType } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { Omit } from '../../../common/utility_types'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; @@ -50,7 +49,7 @@ export const getDefaultWhenTooltipIsUnspecified = ({ /** * Renders the content of the draggable, wrapped in a tooltip */ -const Content = pure<{ +const Content = React.memo<{ children?: React.ReactNode; field: string; tooltipContent?: React.ReactNode; @@ -83,7 +82,7 @@ Content.displayName = 'Content'; * prevent a tooltip from being displayed, or pass arbitrary content * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ -export const DefaultDraggable = pure( +export const DefaultDraggable = React.memo( ({ id, field, value, name, children, tooltipContent, queryValue }) => value != null ? ( & { * prevent a tooltip from being displayed, or pass arbitrary content * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data */ -export const DraggableBadge = pure( +export const DraggableBadge = React.memo( ({ contextId, eventId, diff --git a/x-pack/legacy/plugins/siem/public/components/duration/index.tsx b/x-pack/legacy/plugins/siem/public/components/duration/index.tsx index 06446a152bea8..15e6246f1f1ad 100644 --- a/x-pack/legacy/plugins/siem/public/components/duration/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/duration/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { DefaultDraggable } from '../draggables'; import { FormattedDuration } from '../formatted_duration'; @@ -16,7 +15,7 @@ export const EVENT_DURATION_FIELD_NAME = 'event.duration'; * Renders draggable text containing the value of a field representing a * duration of time, (e.g. `event.duration`) */ -export const Duration = pure<{ +export const Duration = React.memo<{ contextId: string; eventId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap index f343316d88c46..b03670b2b1cd4 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap @@ -1,11 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Embeddable it renders 1`] = ` - - +
+

Test content

- - +
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap index e88693b292a5d..6d02ccb1c6eb9 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap @@ -1,9 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EmbeddableHeader it renders 1`] = ` - - - +
+ + + +
+ Test title +
+
+
+
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx index 49f5306dc1b60..c0d70754e78bd 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable.test.tsx @@ -9,7 +9,6 @@ import toJson from 'enzyme-to-json'; import React from 'react'; import '../../mock/ui_settings'; -import { TestProviders } from '../../mock'; import { Embeddable } from './embeddable'; jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -17,11 +16,9 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('Embeddable', () => { test('it renders', () => { const wrapper = shallow( - - -

{'Test content'}

-
-
+ +

{'Test content'}

+
); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx index 4536da3ba7b97..6387de30aa265 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embeddable_header.test.tsx @@ -16,11 +16,7 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('EmbeddableHeader', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap index 2ef4d9df89a1b..9d39b6e59365f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap @@ -1,18 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PointToolTipContent renders correctly against snapshot 1`] = ` - - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 1733fb3aa7480..5e1eae1649b41 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -46,7 +46,7 @@ describe('PointToolTipContent', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('PointToolTipContentComponent'))).toMatchSnapshot(); }); test('renders array filter correctly', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap index 7e1da6ae7ace3..9b6bfb1752a20 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap @@ -1,9 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders correctly 1`] = ` - + + + Do Something + + + + } + title={ +

+ My Super Title +

+ } /> `; diff --git a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx index 9c3dd462de153..ef2b76c9aad1c 100644 --- a/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/empty_page/index.tsx @@ -6,7 +6,6 @@ import { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, IconType } from '@elastic/eui'; import React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; const EmptyPrompt = styled(EuiEmptyPrompt)` @@ -29,7 +28,7 @@ interface EmptyPageProps { title: string; } -export const EmptyPage = pure( +export const EmptyPage = React.memo( ({ actionPrimaryIcon, actionPrimaryLabel, diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap index bfb10fc385c08..4cf7cbb43cdc7 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -1,692 +1,1544 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EventDetails rendering should match snapshot 1`] = ` - - + , + "id": "table-view", + "name": "Table", } } - columnHeaders={ + tabs={ Array [ Object { - "aggregatable": true, - "category": "base", - "columnHeaderType": "not-filtered", - "description": "Date/time when the event originated. + "content": , + "id": "table-view", + "name": "Table", }, Object { - "field": "destination.port", - "originalValue": 902, - "values": Array [ - "902", - ], + "content": , + "id": "json-view", + "name": "JSON View", }, ] } - id="Y-6TfmcB0WOhS6qyMv3s" - onUpdateColumns={[MockFunction]} - onViewSelected={[MockFunction]} - timelineId="test" - toggleColumn={[MockFunction]} - view="table-view" /> - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap index a788b60afd6b3..caa7853fd9ec0 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -1,150 +1,52 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`JSON View rendering should match snapshot 1`] = ` - + +}" + width="100%" + /> + `; diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx index fb1f9f0cd4e64..d8c0e46d8480b 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx @@ -21,19 +21,17 @@ describe('EventDetails', () => { describe('rendering', () => { test('should match snapshot', () => { const wrapper = shallow( - - - + ); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx index 05690a0d20d92..519f56adff2d2 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/json_view.tsx @@ -7,7 +7,6 @@ import { EuiCodeEditor } from '@elastic/eui'; import { set } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DetailItem } from '../../graphql/types'; @@ -23,7 +22,7 @@ const JsonEditor = styled.div` JsonEditor.displayName = 'JsonEditor'; -export const JsonView = pure(({ data }) => ( +export const JsonView = React.memo(({ data }) => ( (({ leftMargin = true }) => leftMargin ? ( diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap index 6ae9268966480..2ff93b2ecada4 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap @@ -1,220 +1,126 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Field Renderers #autonomousSystemRenderer it renders correctly against snapshot 1`] = ` - - + + + + - - - - - / - - - - - - + / + + + + +
`; exports[`Field Renderers #dateRenderer it renders correctly against snapshot 1`] = ` - - + - + `; exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1`] = ` - - - - raspberrypi - - - + `; exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot 1`] = ` - - - - raspberrypi - - - + `; exports[`Field Renderers #locationRenderer it renders correctly against snapshot 1`] = ` - - + + + + ,  + - - - - ,  - - - - - + + + `; exports[`Field Renderers #reputationRenderer it renders correctly against snapshot 1`] = ` - talosIntelligence.com - + `; exports[`Field Renderers #whoisRenderer it renders correctly against snapshot 1`] = ` - - - iana.org - - + iana.org + `; diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx index 0fd63bc3f2bf2..2d69db82405ba 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx @@ -32,9 +32,7 @@ describe('Field Renderers', () => { describe('#locationRenderer', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - - {locationRenderer(['source.geo.city_name', 'source.geo.region_name'], mockData.complete)} - + locationRenderer(['source.geo.city_name', 'source.geo.region_name'], mockData.complete) ); expect(toJson(wrapper)).toMatchSnapshot(); @@ -59,9 +57,7 @@ describe('Field Renderers', () => { describe('#dateRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallow( - {dateRenderer(mockData.complete.source!.firstSeen)} - ); + const wrapper = shallow(dateRenderer(mockData.complete.source!.firstSeen)); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -78,9 +74,7 @@ describe('Field Renderers', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - - {autonomousSystemRenderer(mockData.complete.source!.autonomousSystem!, FlowTarget.source)} - + autonomousSystemRenderer(mockData.complete.source!.autonomousSystem!, FlowTarget.source) ); expect(toJson(wrapper)).toMatchSnapshot(); @@ -113,9 +107,7 @@ describe('Field Renderers', () => { ip: null, }; test('it renders correctly against snapshot', () => { - const wrapper = shallow( - {hostNameRenderer(mockData.complete.host, '10.10.10.10')} - ); + const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -158,9 +150,7 @@ describe('Field Renderers', () => { ip: ['10.10.10.10'], }; test('it renders correctly against snapshot', () => { - const wrapper = shallow( - {hostNameRenderer(mockData.complete.host, '10.10.10.10')} - ); + const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -194,9 +184,7 @@ describe('Field Renderers', () => { describe('#whoisRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallowWithIntl( - {whoisRenderer('10.10.10.10')} - ); + const wrapper = shallowWithIntl(whoisRenderer('10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -208,7 +196,7 @@ describe('Field Renderers', () => { {reputationRenderer('10.10.10.10')} ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('DragDropContext'))).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx index 5df961dfceeb5..80d68dfe1b731 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.tsx @@ -8,9 +8,8 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiText } from ' import { FormattedMessage } from '@kbn/i18n/react'; import { getOr } from 'lodash/fp'; import React, { Fragment, useState } from 'react'; -import { pure } from 'recompose'; - import styled from 'styled-components'; + import { AutonomousSystem, FlowTarget, HostEcsFields, IpOverviewData } from '../../graphql/types'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { DefaultDraggable } from '../draggables'; @@ -151,7 +150,7 @@ interface DefaultFieldRendererProps { // TODO: This causes breaks between elements until the ticket below is fixed // https://github.com/elastic/ingest-dev/issues/474 -export const DefaultFieldRenderer = pure( +export const DefaultFieldRenderer = React.memo( ({ attrName, displayCount = 1, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx index 8d4e3b3928492..7b8451db2212f 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/category.tsx @@ -5,7 +5,6 @@ */ import { EuiInMemoryTable } from '@elastic/eui'; -import { pure } from 'recompose'; import * as React from 'react'; import styled from 'styled-components'; @@ -33,7 +32,7 @@ interface Props { width: number; } -export const Category = pure( +export const Category = React.memo( ({ categoryId, filteredBrowserFields, fieldItems, timelineId, width }) => ( <> (({ filteredBrowserFields, categoryId, timelineId }) => ( - - - -
{categoryId}
-
-
- - - - - {getFieldCount(filteredBrowserFields[categoryId])} - - - -
-)); +export const CategoryTitle = React.memo( + ({ filteredBrowserFields, categoryId, timelineId }) => ( + + + +
{categoryId}
+
+
+ + + + + {getFieldCount(filteredBrowserFields[categoryId])} + + + +
+ ) +); CategoryTitle.displayName = 'CategoryTitle'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx index 4cc5537bec343..170cf324ca6d8 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.tsx @@ -5,7 +5,6 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { pure } from 'recompose'; import * as React from 'react'; import styled from 'styled-components'; @@ -59,7 +58,7 @@ type Props = Pick void; }; -export const FieldsPane = pure( +export const FieldsPane = React.memo( ({ columnHeaders, filteredBrowserFields, diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx index ae9109bffe0db..8acb19970c268 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/header.tsx @@ -13,7 +13,6 @@ import { EuiTitle, } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -65,7 +64,7 @@ interface Props { timelineId: string; } -const CountRow = pure>(({ filteredBrowserFields }) => ( +const CountRow = React.memo>(({ filteredBrowserFields }) => ( >(({ filteredBrowserFi CountRow.displayName = 'CountRow'; -const TitleRow = pure<{ +const TitleRow = React.memo<{ isEventViewer?: boolean; onOutsideClick: () => void; onUpdateColumns: OnUpdateColumns; @@ -121,7 +120,7 @@ const TitleRow = pure<{ TitleRow.displayName = 'TitleRow'; -export const Header = pure( +export const Header = React.memo( ({ isEventViewer, isSearching, diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap index 56432cb25c189..35fe74abff284 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap @@ -1,9 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`rendering renders correctly 1`] = ` - -

- Additional filters here. -

-
+ + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx index adbd904c5c325..7f377a57c3e9b 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.test.tsx @@ -9,7 +9,7 @@ import toJson from 'enzyme-to-json'; import React from 'react'; import '../../mock/match_media'; -import { FiltersGlobal } from './index'; +import { FiltersGlobal } from './filters_global'; describe('rendering', () => { test('renders correctly', () => { @@ -18,6 +18,7 @@ describe('rendering', () => {

{'Additional filters here.'}

); + expect(toJson(wrapper)).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx index bdda8497a8bcb..edf6f7f01ab2e 100644 --- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx +++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx @@ -7,7 +7,6 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import React from 'react'; import { Sticky } from 'react-sticky'; -import { pure } from 'recompose'; import styled, { css } from 'styled-components'; import { gutterTimeline } from '../../lib/helpers'; @@ -42,7 +41,7 @@ export interface FiltersGlobalProps { children: React.ReactNode; } -export const FiltersGlobal = pure(({ children }) => ( +export const FiltersGlobal = React.memo(({ children }) => ( {({ style, isSticky }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap index 9553ec5b7654e..ee76657c8d27a 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap @@ -1,8 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Select Flow Direction rendering it renders the basic group button for uni-direction and bi-direction 1`] = ` - + + + Unidirectional + + + Bidirectional + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap index 46053008ea09c..a9b48c8ee16be 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap @@ -1,11 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`FlowTargetSelect Component rendering it renders the FlowTargetSelect 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx index d5370c218a2de..2b826164063be 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/flow_direction_select.tsx @@ -7,7 +7,6 @@ import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; import React from 'react'; -import { pure } from 'recompose'; import { FlowDirection } from '../../graphql/types'; import * as i18n from './translations'; @@ -17,7 +16,7 @@ interface Props { onChangeDirection: (value: FlowDirection) => void; } -export const FlowDirectionSelect = pure(({ onChangeDirection, selectedDirection }) => ( +export const FlowDirectionSelect = React.memo(({ onChangeDirection, selectedDirection }) => ( ( +export const FlowTargetSelect = React.memo( ({ id, isLoading = false, diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap index 3aa9fd1b962b5..abdc4f4681294 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap @@ -1,16 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Flyout rendering it renders correctly against snapshot 1`] = ` - - - + `; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx index ddc3e4f15938a..86a8952a10efa 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.test.tsx @@ -37,7 +37,7 @@ describe('Flyout', () => { /> ); - expect(toJson(wrapper)).toMatchSnapshot(); + expect(toJson(wrapper.find('Flyout'))).toMatchSnapshot(); }); test('it renders the default flyout state as a button', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index aae8f67997156..2d347830d5b1b 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -124,3 +124,5 @@ const mapStateToProps = (state: State, { timelineId }: OwnProps) => { export const Flyout = connect(mapStateToProps, { showTimeline: timelineActions.showTimeline, })(FlyoutComponent); + +Flyout.displayName = 'Flyout'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap index 31eaf4f56d7bc..efa682cd4d18e 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap @@ -1,22 +1,20 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Pane renders correctly against snapshot 1`] = ` - - - - I am a child of flyout - - - + + + I am a child of flyout + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index 65233e55901ff..acea2d1cce468 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -44,7 +44,7 @@ describe('Pane', () => { ); - expect(toJson(EmptyComponent)).toMatchSnapshot(); + expect(toJson(EmptyComponent.find('Pane'))).toMatchSnapshot(); }); test('it should NOT let the flyout expand to take up the full width of the element that contains it', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx index 4b5ceb25befa4..f2f0cf4f980f3 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.tsx @@ -182,3 +182,5 @@ FlyoutPaneComponent.displayName = 'FlyoutPaneComponent'; export const Pane = connect(null, { applyDeltaToWidth: timelineActions.applyDeltaToWidth, })(FlyoutPaneComponent); + +Pane.displayName = 'Pane'; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx index 71820c62dd528..a517820361f9f 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_bytes/index.test.tsx @@ -21,6 +21,10 @@ jest.mock('../../lib/settings/use_kibana_ui_setting', () => ({ describe('formatted_bytes', () => { describe('PreferenceFormattedBytes', () => { describe('rendering', () => { + beforeEach(() => { + mockUseKibanaUiSetting.mockClear(); + }); + const bytes = '2806422'; test('renders correctly against snapshot', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap index 0f9cf1ba89f9c..d196a23bff5bf 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`formatted_date PreferenceFormattedDate rendering renders correctly against snapshot 1`] = ` - +> + 2019-02-25T22:27:05.000Z + `; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx index bb0b947f149f4..df361a06d3805 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.test.tsx @@ -38,7 +38,8 @@ describe('formatted_date', () => { .format(config.dateFormat); test('renders correctly against snapshot', () => { - const wrapper = shallow(); + mockUseKibanaUiSetting.mockImplementation(() => [null]); + const wrapper = mount(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx index 32c064096fcf9..37bf3653f3b62 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_date/index.tsx @@ -7,7 +7,6 @@ import moment from 'moment-timezone'; import * as React from 'react'; import { FormattedRelative } from '@kbn/i18n/react'; -import { pure } from 'recompose'; import { DEFAULT_DATE_FORMAT, @@ -19,7 +18,7 @@ import { getOrEmptyTagFromValue } from '../empty_value'; import { LocalizedDateTooltip } from '../localized_date_tooltip'; import { getMaybeDate } from './maybe_date'; -export const PreferenceFormattedDate = pure<{ value: Date }>(({ value }) => { +export const PreferenceFormattedDate = React.memo<{ value: Date }>(({ value }) => { const [dateFormat] = useKibanaUiSetting(DEFAULT_DATE_FORMAT); const [dateFormatTz] = useKibanaUiSetting(DEFAULT_DATE_FORMAT_TZ); const [timezone] = useKibanaUiSetting(DEFAULT_TIMEZONE_BROWSER); @@ -43,7 +42,7 @@ PreferenceFormattedDate.displayName = 'PreferenceFormattedDate'; * - a long representation of the date that includes the day of the week (e.g. Thursday, March 21, 2019 6:47pm) * - the raw date value (e.g. 2019-03-22T00:47:46Z) */ -export const FormattedDate = pure<{ +export const FormattedDate = React.memo<{ fieldName: string; value?: string | number | null; }>( diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx index c97fc7bdc2428..8afbafe57af4a 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_duration/index.tsx @@ -5,12 +5,11 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { getFormattedDurationString } from './helpers'; import { FormattedDurationTooltip } from './tooltip'; -export const FormattedDuration = pure<{ +export const FormattedDuration = React.memo<{ maybeDurationNanoseconds: string | number | object | undefined | null; tooltipTitle?: string; }>(({ maybeDurationNanoseconds, tooltipTitle }) => ( diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx index 08f4a412caf51..1372b3ef10920 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_duration/tooltip/index.tsx @@ -6,7 +6,6 @@ import { EuiToolTip } from '@elastic/eui'; import * as React from 'react'; -import { pure } from 'recompose'; import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; @@ -18,7 +17,7 @@ const P = styled.p` P.displayName = 'P'; -export const FormattedDurationTooltipContent = pure<{ +export const FormattedDurationTooltipContent = React.memo<{ maybeDurationNanoseconds: string | number | object | undefined | null; tooltipTitle?: string; }>(({ maybeDurationNanoseconds, tooltipTitle }) => ( @@ -35,7 +34,7 @@ export const FormattedDurationTooltipContent = pure<{ FormattedDurationTooltipContent.displayName = 'FormattedDurationTooltipContent'; -export const FormattedDurationTooltip = pure<{ +export const FormattedDurationTooltip = React.memo<{ children: JSX.Element; maybeDurationNanoseconds: string | number | object | undefined | null; tooltipTitle?: string; diff --git a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx b/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx index 81f5cbfe2308b..8dcb558122d01 100644 --- a/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/formatted_ip/index.tsx @@ -6,7 +6,6 @@ import { isArray, isEmpty, isString, uniq } from 'lodash/fp'; import * as React from 'react'; -import { pure } from 'recompose'; import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; @@ -60,7 +59,7 @@ const getDataProvider = ({ and: [], }); -const NonDecoratedIp = pure<{ +const NonDecoratedIp = React.memo<{ contextId: string; eventId: string; fieldName: string; @@ -92,7 +91,7 @@ const NonDecoratedIp = pure<{ NonDecoratedIp.displayName = 'NonDecoratedIp'; -const AddressLinks = pure<{ +const AddressLinks = React.memo<{ addresses: string[]; contextId: string; eventId: string; @@ -128,7 +127,7 @@ const AddressLinks = pure<{ AddressLinks.displayName = 'AddressLinks'; -export const FormattedIp = pure<{ +export const FormattedIp = React.memo<{ contextId: string; eventId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap index 665a5c75f3684..849f3616524cc 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap @@ -1,7 +1,107 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderGlobal it renders 1`] = ` - - - + + + + + + + + + + + + + + + + + + + + + + Add data + + + + + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx index ebd1da634ed1a..b3eb599af9407 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.test.tsx @@ -8,7 +8,6 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import React from 'react'; -import { TestProviders } from '../../mock'; import '../../mock/match_media'; import '../../mock/ui_settings'; import { HeaderGlobal } from './index'; @@ -23,11 +22,7 @@ jest.mock('../search_bar', () => ({ describe('HeaderGlobal', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap index 0fe2890dc9f24..a91d8fce87dac 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap @@ -1,23 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderPage it renders 1`] = ` - - + -

- Test supplement -

-
-
+ + +

+ Test title + + +

+
+ + +
+ +

+ Test supplement +

+
+ + `; diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx index 9c50a915b7ba8..c20f3c7185e66 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx @@ -18,17 +18,15 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('HeaderPage', () => { test('it renders', () => { const wrapper = shallow( - - -

{'Test supplement'}

-
-
+ +

{'Test supplement'}

+
); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap index ecd2b15a841f6..d4c3763f51460 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap @@ -1,9 +1,26 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderSection it renders 1`] = ` - - - +
+ + + + + +

+ Test title +

+
+
+
+
+
+
`; diff --git a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx index 4a6da9c80968f..8606758c68d2c 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_section/index.test.tsx @@ -17,11 +17,7 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('HeaderSection', () => { test('it renders', () => { - const wrapper = shallow( - - - - ); + const wrapper = shallow(); expect(toJson(wrapper)).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx index 43fd8e653f3d8..d42ee08e86407 100644 --- a/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/help_menu/index.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect } from 'react'; -import { pure } from 'recompose'; +import React, { useEffect } from 'react'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; -export const HelpMenu = pure<{}>(() => { +export const HelpMenu = React.memo(() => { useEffect(() => { chrome.helpExtension.set({ appName: i18n.translate('xpack.siem.chrome.help.appName', { diff --git a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx index 56bd86310acad..6908aba542e4c 100644 --- a/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/inspect/index.tsx @@ -11,7 +11,6 @@ import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; import styled from 'styled-components'; -import { pure } from 'recompose'; import { inputsModel, inputsSelectors, State } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; import { inputsActions } from '../../store/inputs'; @@ -58,7 +57,7 @@ interface InspectButtonDispatch { type InspectButtonProps = OwnProps & InspectButtonReducer & InspectButtonDispatch; -const InspectButtonComponent = pure( +const InspectButtonComponent = React.memo( ({ compact = false, inputId = 'global', diff --git a/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap index d75a0f054775a..0199742242e59 100644 --- a/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap @@ -1,10 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Port renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/legacy/plugins/siem/public/components/ip/index.tsx b/x-pack/legacy/plugins/siem/public/components/ip/index.tsx index ceec48951a198..8c327989963b4 100644 --- a/x-pack/legacy/plugins/siem/public/components/ip/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ip/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; @@ -18,7 +17,7 @@ const IP_FIELD_TYPE = 'ip'; * Renders text containing a draggable IP address (e.g. `source.ip`, * `destination.ip`) that contains a hyperlink */ -export const Ip = pure<{ +export const Ip = React.memo<{ contextId: string; eventId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx index 3148efbb3050a..950ab252ad0bd 100644 --- a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { pure } from 'recompose'; import styled from 'styled-components'; import { DraggableBadge } from '../draggables'; @@ -27,7 +26,7 @@ Ja3FingerprintLabel.displayName = 'Ja3FingerprintLabel'; * using TLS traffic to be identified, which is possible because SSL * negotiations happen in the clear */ -export const Ja3Fingerprint = pure<{ +export const Ja3Fingerprint = React.memo<{ eventId: string; contextId: string; fieldName: string; diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap index 5902768383cb0..c5086c8cde285 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap @@ -1,14 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LinkIcon it renders 1`] = ` - - + + Test link - - + + `; diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx index 451db49028ee1..7f9133a0de7c0 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/index.test.tsx @@ -17,11 +17,9 @@ jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('LinkIcon', () => { test('it renders', () => { const wrapper = shallow( - - - {'Test link'} - - + + {'Test link'} + ); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 0125b52e3ad33..5a7f6ef1274c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom'; -import { pure } from 'recompose'; import { SiemPageName } from '../../pages/home/types'; import { HostsTableType } from '../../store/hosts/model'; @@ -26,7 +25,7 @@ interface LinkToPageProps { match: RouteMatch<{}>; } -export const LinkToPage = pure(({ match }) => ( +export const LinkToPage = React.memo(({ match }) => ( ( +export const HostDetailsLink = React.memo<{ children?: React.ReactNode; hostName: string }>( ({ children, hostName }) => ( {children ? children : hostName} @@ -22,7 +21,7 @@ export const HostDetailsLink = pure<{ children?: React.ReactNode; hostName: stri HostDetailsLink.displayName = 'HostDetailsLink'; -export const IPDetailsLink = pure<{ children?: React.ReactNode; ip: string }>( +export const IPDetailsLink = React.memo<{ children?: React.ReactNode; ip: string }>( ({ children, ip }) => ( {children ? children : ip} @@ -33,7 +32,7 @@ export const IPDetailsLink = pure<{ children?: React.ReactNode; ip: string }>( IPDetailsLink.displayName = 'IPDetailsLink'; // External Links -export const GoogleLink = pure<{ children?: React.ReactNode; link: string }>( +export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( {children ? children : link} @@ -43,7 +42,7 @@ export const GoogleLink = pure<{ children?: React.ReactNode; link: string }>( GoogleLink.displayName = 'GoogleLink'; -export const PortOrServiceNameLink = pure<{ +export const PortOrServiceNameLink = React.memo<{ children?: React.ReactNode; portOrServiceName: number | string; }>(({ children, portOrServiceName }) => ( @@ -60,21 +59,22 @@ export const PortOrServiceNameLink = pure<{ PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; -export const Ja3FingerprintLink = pure<{ children?: React.ReactNode; ja3Fingerprint: string }>( - ({ children, ja3Fingerprint }) => ( - - {children ? children : ja3Fingerprint} - - ) -); +export const Ja3FingerprintLink = React.memo<{ + children?: React.ReactNode; + ja3Fingerprint: string; +}>(({ children, ja3Fingerprint }) => ( + + {children ? children : ja3Fingerprint} + +)); Ja3FingerprintLink.displayName = 'Ja3FingerprintLink'; -export const CertificateFingerprintLink = pure<{ +export const CertificateFingerprintLink = React.memo<{ children?: React.ReactNode; certificateFingerprint: string; }>(({ children, certificateFingerprint }) => ( @@ -91,7 +91,7 @@ export const CertificateFingerprintLink = pure<{ CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; -export const ReputationLink = pure<{ children?: React.ReactNode; domain: string }>( +export const ReputationLink = React.memo<{ children?: React.ReactNode; domain: string }>( ({ children, domain }) => ( ( +export const VirusTotalLink = React.memo<{ children?: React.ReactNode; link: string }>( ({ children, link }) => ( VirusTotalLink.displayName = 'VirusTotalLink'; -export const WhoIsLink = pure<{ children?: React.ReactNode; domain: string }>( +export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( ({ children, domain }) => ( {children ? children : domain} diff --git a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap index 440193c9e0dfd..0885f15b1efba 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap @@ -1,11 +1,36 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`rendering renders correctly 1`] = ` - - Loading - + + + + + + +

+ Loading +

+
+
+
+ `; diff --git a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx index 55628fe2e8d33..be2ce3dde951c 100644 --- a/x-pack/legacy/plugins/siem/public/components/loader/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/loader/index.tsx @@ -14,7 +14,6 @@ import { } from '@elastic/eui'; import { rgba } from 'polished'; import React from 'react'; -import { pure } from 'recompose'; import styled, { css } from 'styled-components'; const Aside = styled.aside<{ overlay?: boolean; overlayBackground?: string }>` @@ -56,9 +55,10 @@ export interface LoaderProps { overlay?: boolean; overlayBackground?: string; size?: EuiLoadingSpinnerSize; + children?: React.ReactChild; } -export const Loader = pure(({ children, overlay, overlayBackground, size }) => ( +export const Loader = React.memo(({ children, overlay, overlayBackground, size }) => (