From 3722bea42f03fc7d2799d88fed6bb1aaba945055 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 8 Feb 2021 12:53:54 -0700 Subject: [PATCH 01/19] [Maps] clamp MVT too many features polygon to tile boundary (#90444) * [Maps] clamp MVT too many features polygon to tile boundary * add mapbox_styles to index.js Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/server/mvt/get_tile.ts | 84 ++-- x-pack/test/functional/apps/maps/index.js | 1 + x-pack/test/functional/apps/maps/joins.js | 34 -- .../functional/apps/maps/mapbox_styles.js | 358 +++++++++++------- 4 files changed, 242 insertions(+), 235 deletions(-) diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 3116838d26fb5..50c2014275a0f 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -25,7 +25,7 @@ import { import { convertRegularRespToGeoJson, hitsToGeoJson } from '../../common/elasticsearch_util'; import { flattenHit } from './util'; -import { ESBounds, tile2lat, tile2long, tileToESBbox } from '../../common/geo_tile_utils'; +import { ESBounds, tileToESBbox } from '../../common/geo_tile_utils'; import { getCentroidFeatures } from '../../common/get_centroid_features'; export async function getGridTile({ @@ -53,35 +53,14 @@ export async function getGridTile({ geoFieldType: ES_GEO_FIELD_TYPE; searchSessionId?: string; }): Promise { - const esBbox: ESBounds = tileToESBbox(x, y, z); try { - let bboxFilter; - if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - bboxFilter = { - geo_bounding_box: { - [geometryFieldName]: esBbox, - }, - }; - } else if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { - const geojsonPolygon = tileToGeoJsonPolygon(x, y, z); - bboxFilter = { - geo_shape: { - [geometryFieldName]: { - shape: geojsonPolygon, - relation: 'INTERSECTS', - }, - }, - }; - } else { - throw new Error(`${geoFieldType} is not valid geo field-type`); - } - requestBody.query.bool.filter.push(bboxFilter); - + const tileBounds: ESBounds = tileToESBbox(x, y, z); + requestBody.query.bool.filter.push(getTileSpatialFilter(geometryFieldName, tileBounds)); requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.precision = Math.min( z + SUPER_FINE_ZOOM_DELTA, MAX_ZOOM ); - requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = esBbox; + requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = tileBounds; const response = await context .search!.search( @@ -134,14 +113,9 @@ export async function getTile({ }): Promise { let features: Feature[]; try { - requestBody.query.bool.filter.push({ - geo_shape: { - [geometryFieldName]: { - shape: tileToGeoJsonPolygon(x, y, z), - relation: 'INTERSECTS', - }, - }, - }); + requestBody.query.bool.filter.push( + getTileSpatialFilter(geometryFieldName, tileToESBbox(x, y, z)) + ); const searchOptions = { sessionId: searchSessionId, @@ -193,7 +167,8 @@ export async function getTile({ [KBN_TOO_MANY_FEATURES_PROPERTY]: true, }, geometry: esBboxToGeoJsonPolygon( - bboxResponse.rawResponse.aggregations.data_bounds.bounds + bboxResponse.rawResponse.aggregations.data_bounds.bounds, + tileToESBbox(x, y, z) ), }, ]; @@ -244,32 +219,31 @@ export async function getTile({ } } -function tileToGeoJsonPolygon(x: number, y: number, z: number): Polygon { - const wLon = tile2long(x, z); - const sLat = tile2lat(y + 1, z); - const eLon = tile2long(x + 1, z); - const nLat = tile2lat(y, z); - +function getTileSpatialFilter(geometryFieldName: string, tileBounds: ESBounds): unknown { return { - type: 'Polygon', - coordinates: [ - [ - [wLon, sLat], - [wLon, nLat], - [eLon, nLat], - [eLon, sLat], - [wLon, sLat], - ], - ], + geo_shape: { + [geometryFieldName]: { + shape: { + type: 'envelope', + // upper left and lower right points of the shape to represent a bounding rectangle in the format [[minLon, maxLat], [maxLon, minLat]] + coordinates: [ + [tileBounds.top_left.lon, tileBounds.top_left.lat], + [tileBounds.bottom_right.lon, tileBounds.bottom_right.lat], + ], + }, + relation: 'INTERSECTS', + }, + }, }; } -function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon { - let minLon = esBounds.top_left.lon; - const maxLon = esBounds.bottom_right.lon; +function esBboxToGeoJsonPolygon(esBounds: ESBounds, tileBounds: ESBounds): Polygon { + // Intersecting geo_shapes may push bounding box outside of tile so need to clamp to tile bounds. + let minLon = Math.max(esBounds.top_left.lon, tileBounds.top_left.lon); + const maxLon = Math.min(esBounds.bottom_right.lon, tileBounds.bottom_right.lon); minLon = minLon > maxLon ? minLon - 360 : minLon; // fixes an ES bbox to straddle dateline - const minLat = esBounds.bottom_right.lat; - const maxLat = esBounds.top_left.lat; + const minLat = Math.max(esBounds.bottom_right.lat, tileBounds.bottom_right.lat); + const maxLat = Math.min(esBounds.top_left.lat, tileBounds.top_left.lat); return { type: 'Polygon', diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index d76afb7ebdc24..dd20ed58afbc6 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -47,6 +47,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./es_geo_grid_source')); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); + loadTestFile(require.resolve('./mapbox_styles')); loadTestFile(require.resolve('./mvt_scaling')); loadTestFile(require.resolve('./mvt_super_fine')); loadTestFile(require.resolve('./add_layer_panel')); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 094f5335cd05f..49717016f9c60 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -7,8 +7,6 @@ import expect from '@kbn/expect'; -import { MAPBOX_STYLES } from './mapbox_styles'; - const JOIN_PROPERTY_NAME = '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'; const EXPECTED_JOIN_VALUES = { alpha: 10, @@ -18,10 +16,6 @@ const EXPECTED_JOIN_VALUES = { }; const VECTOR_SOURCE_ID = 'n1t6f'; -const CIRCLE_STYLE_LAYER_INDEX = 0; -const FILL_STYLE_LAYER_INDEX = 2; -const LINE_STYLE_LAYER_INDEX = 3; -const TOO_MANY_FEATURES_LAYER_INDEX = 4; export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); @@ -95,34 +89,6 @@ export default function ({ getPageObjects, getService }) { }); }); - it('should style fills, points, lines, and bounding-boxes independently', async () => { - const mapboxStyle = await PageObjects.maps.getMapboxStyle(); - const layersForVectorSource = mapboxStyle.layers.filter((mbLayer) => { - return mbLayer.id.startsWith(VECTOR_SOURCE_ID); - }); - - //circle layer for points - expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.POINT_LAYER); - - //fill layer - expect(layersForVectorSource[FILL_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.FILL_LAYER); - - //line layer for borders - expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql(MAPBOX_STYLES.LINE_LAYER); - - //Too many features layer (this is a static style config) - expect(layersForVectorSource[TOO_MANY_FEATURES_LAYER_INDEX]).to.eql({ - id: 'n1t6f_toomanyfeatures', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: ['==', ['get', '__kbn_too_many_features__'], true], - layout: { visibility: 'visible' }, - paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, - }); - }); - it('should flag only the joined features as visible', async () => { const mapboxStyle = await PageObjects.maps.getMapboxStyle(); const vectorSource = mapboxStyle.sources[VECTOR_SOURCE_ID]; diff --git a/x-pack/test/functional/apps/maps/mapbox_styles.js b/x-pack/test/functional/apps/maps/mapbox_styles.js index d4496f13b8bef..b483b95e0ca1f 100644 --- a/x-pack/test/functional/apps/maps/mapbox_styles.js +++ b/x-pack/test/functional/apps/maps/mapbox_styles.js @@ -5,176 +5,242 @@ * 2.0. */ -export const MAPBOX_STYLES = { - POINT_LAYER: { - id: 'n1t6f_circle', - type: 'circle', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'circle-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + const security = getService('security'); + + describe('mapbox styles', () => { + let mapboxStyle; + before(async () => { + await security.testUser.setRoles( + ['global_maps_all', 'geoshape_data_reader', 'meta_for_geoshape_data_reader'], + false + ); + await PageObjects.maps.loadSavedMap('join example'); + mapboxStyle = await PageObjects.maps.getMapboxStyle(); + }); + + after(async () => { + await inspector.close(); + await security.testUser.restoreDefaults(); + }); + + it('should style circle layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_circle'; + }); + expect(layer).to.eql({ + id: 'n1t6f_circle', + type: 'circle', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', - [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, - ], - 2, + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + ['any', ['==', ['geometry-type'], 'Point'], ['==', ['geometry-type'], 'MultiPoint']], + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'circle-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'circle-opacity': 0.75, - 'circle-stroke-color': '#41937c', - 'circle-stroke-opacity': 0.75, - 'circle-stroke-width': 1, - 'circle-radius': 10, - }, - }, - FILL_LAYER: { - id: 'n1t6f_fill', - type: 'fill', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - ['any', ['==', ['geometry-type'], 'Polygon'], ['==', ['geometry-type'], 'MultiPolygon']], - ], - ], - layout: { visibility: 'visible' }, - paint: { - 'fill-color': [ - 'interpolate', - ['linear'], - [ - 'coalesce', + 'circle-opacity': 0.75, + 'circle-stroke-color': '#41937c', + 'circle-stroke-opacity': 0.75, + 'circle-stroke-width': 1, + 'circle-radius': 10, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_fill'; + }); + expect(layer).to.eql({ + id: 'n1t6f_fill', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], [ - 'case', + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], [ - '==', - ['feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1'], - null, + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], ], - 2, + ], + ], + layout: { visibility: 'visible' }, + paint: { + 'fill-color': [ + 'interpolate', + ['linear'], [ - 'max', + 'coalesce', [ - 'min', + 'case', [ - 'to-number', + '==', [ 'feature-state', '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', ], + null, + ], + 2, + [ + 'max', + [ + 'min', + [ + 'to-number', + [ + 'feature-state', + '__kbnjoin__max_of_prop1__855ccb86-fe42-11e8-8eb2-f2801f1b9fd1', + ], + ], + 12, + ], + 3, ], - 12, ], - 3, + 2, + ], + 2, + 'rgba(0,0,0,0)', + 3, + '#ecf1f7', + 4.125, + '#d9e3ef', + 5.25, + '#c5d5e7', + 6.375, + '#b2c7df', + 7.5, + '#9eb9d8', + 8.625, + '#8bacd0', + 9.75, + '#769fc8', + 10.875, + '#6092c0', + ], + 'fill-opacity': 0.75, + }, + }); + }); + + it('should style fill layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_line'; + }); + expect(layer).to.eql({ + id: 'n1t6f_line', + type: 'line', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: [ + 'all', + ['==', ['get', '__kbn_isvisibleduetojoin__'], true], + [ + 'all', + ['!=', ['get', '__kbn_too_many_features__'], true], + ['!=', ['get', '__kbn_is_centroid_feature__'], true], + [ + 'any', + ['==', ['geometry-type'], 'Polygon'], + ['==', ['geometry-type'], 'MultiPolygon'], + ['==', ['geometry-type'], 'LineString'], + ['==', ['geometry-type'], 'MultiLineString'], ], ], - 2, - ], - 2, - 'rgba(0,0,0,0)', - 3, - '#ecf1f7', - 4.125, - '#d9e3ef', - 5.25, - '#c5d5e7', - 6.375, - '#b2c7df', - 7.5, - '#9eb9d8', - 8.625, - '#8bacd0', - 9.75, - '#769fc8', - 10.875, - '#6092c0', - ], - 'fill-opacity': 0.75, - }, - }, - LINE_LAYER: { - id: 'n1t6f_line', - type: 'line', - source: 'n1t6f', - minzoom: 0, - maxzoom: 24, - filter: [ - 'all', - ['==', ['get', '__kbn_isvisibleduetojoin__'], true], - [ - 'all', - ['!=', ['get', '__kbn_too_many_features__'], true], - ['!=', ['get', '__kbn_is_centroid_feature__'], true], - [ - 'any', - ['==', ['geometry-type'], 'Polygon'], - ['==', ['geometry-type'], 'MultiPolygon'], - ['==', ['geometry-type'], 'LineString'], - ['==', ['geometry-type'], 'MultiLineString'], ], - ], - ], - layout: { visibility: 'visible' }, - paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, - }, -}; + layout: { visibility: 'visible' }, + paint: { 'line-color': '#41937c', 'line-opacity': 0.75, 'line-width': 1 }, + }); + }); + + it('should style incomplete data layer as expected', async () => { + const layer = mapboxStyle.layers.find((mbLayer) => { + return mbLayer.id === 'n1t6f_toomanyfeatures'; + }); + expect(layer).to.eql({ + id: 'n1t6f_toomanyfeatures', + type: 'fill', + source: 'n1t6f', + minzoom: 0, + maxzoom: 24, + filter: ['==', ['get', '__kbn_too_many_features__'], true], + layout: { visibility: 'visible' }, + paint: { 'fill-pattern': '__kbn_too_many_features_image_id__', 'fill-opacity': 0.75 }, + }); + }); + }); +} From a1a2536b5bb624d9dce989389319d7d527377d79 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 8 Feb 2021 21:26:57 +0100 Subject: [PATCH 02/19] [Uptime] Waterfall filters (#89185) * WIP * Use multi canvas solution * type * fix test * adde unit tests * reduce item to 150 * update margins * use constant * update z-index * added key * wip * wip * wip filters * reorgnaise components * fix issue * update filter * only highlight button * water fall test * styling * fix styling * test * fix types * update test * update ari hidden * added click telemetry for waterfall filters * added input click telemetry * update filter behaviour * fixed typo * fix type * fix styling * persist original resource number in waterfall sidebar when showing only highlighted resources * update waterfall filter collapse checkbox content * update use_bar_charts to work with filtered data * update network request total label to include filtered requests * adjust telemetry * add accessible text * add waterfall chart view telemetry * updated mime type filter label translations * adjust total network requests to use FormattedMessage * adjust translations and tests * use FormattedMessage in NetworkRequestsTotal * ensure sidebar persists when 0 resources match filter * use destructuring in waterfall sidebar item * reset collapse requests checkbox when filters are removed * update license headers Co-authored-by: Dominique Clarke Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/observability/public/index.ts | 1 + .../waterfall/data_formatting.test.ts | 330 +++++++++++------- .../step_detail/waterfall/data_formatting.ts | 65 +++- .../synthetics/step_detail/waterfall/types.ts | 43 +-- .../waterfall_chart_wrapper.test.tsx | 248 +++++++++++++ .../waterfall/waterfall_chart_wrapper.tsx | 102 +++--- .../waterfall/waterfall_filter.test.tsx | 155 ++++++++ .../waterfall/waterfall_filter.tsx | 188 ++++++++++ .../waterfall/waterfall_sidebar_item.tsx | 56 +++ .../waterfalll_sidebar_item.test.tsx | 51 +++ .../waterfall/components/constants.ts | 2 + .../components/middle_truncated_text.test.tsx | 12 +- .../components/middle_truncated_text.tsx | 9 +- .../network_requests_total.test.tsx | 51 ++- .../components/network_requests_total.tsx | 45 ++- .../waterfall/components/sidebar.tsx | 17 +- .../synthetics/waterfall/components/styles.ts | 50 ++- .../waterfall/components/translations.ts | 50 +++ .../components/use_bar_charts.test.tsx | 46 ++- .../waterfall/components/use_bar_charts.ts | 31 +- .../waterfall/components/waterfall.test.tsx | 70 ++-- .../components/waterfall_bar_chart.tsx | 112 ++++++ .../waterfall/components/waterfall_chart.tsx | 221 ++++-------- .../components/waterfall_chart_fixed_axis.tsx | 65 ++++ .../waterfall/context/waterfall_chart.tsx | 11 +- .../uptime/public/hooks/use_chart_theme.ts | 20 ++ .../public/lib/helper/enzyme_helpers.tsx | 45 ++- .../uptime/public/lib/helper/rtl_helpers.tsx | 8 +- 28 files changed, 1632 insertions(+), 472 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx create mode 100644 x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx create mode 100644 x-pack/plugins/uptime/public/hooks/use_chart_theme.ts diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index e9a9bb8146dbf..1db5f62823e9b 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -23,6 +23,7 @@ export { getCoreVitalsComponent, HeaderMenuPortal } from './components/shared/'; export { useTrackPageview, useUiTracker, + useTrackMetric, UiTracker, TrackMetricOptions, METRIC_TYPE, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts index 487daf0332a98..a02116877f49a 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.test.ts @@ -5,10 +5,143 @@ * 2.0. */ -import { colourPalette, getSeriesAndDomain } from './data_formatting'; +import { colourPalette, getSeriesAndDomain, getSidebarItems } from './data_formatting'; import { NetworkItems, MimeType } from './types'; import { WaterfallDataEntry } from '../../waterfall/types'; +const networkItems: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', + status: 200, + mimeType: 'text/css', + requestSentTime: 18098833.175, + requestStartTime: 18098835.439, + loadEndTime: 18098957.145, + timings: { + connect: 81.10800000213203, + wait: 34.577999998873565, + receive: 0.5520000013348181, + send: 0.3600000018195715, + total: 123.97000000055414, + proxy: -1, + blocked: 0.8540000017092098, + queueing: 2.263999998831423, + ssl: 55.38700000033714, + dns: 3.559999997378327, + }, + }, + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + +const networkItemsWithoutFullTimings: NetworkItems = [ + networkItems[0], + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: 2.7929999996558763, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutAnyTimings: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + timings: { + total: -1, + blocked: -1, + ssl: -1, + wait: -1, + connect: -1, + dns: -1, + queueing: -1, + send: -1, + proxy: -1, + receive: -1, + }, + }, +]; + +const networkItemsWithoutTimingsObject: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', + status: 0, + mimeType: 'text/javascript', + requestSentTime: 18098834.097, + loadEndTime: 18098836.889999997, + }, +]; + +const networkItemsWithUncommonMimeType: NetworkItems = [ + { + timestamp: '2021-01-05T19:22:28.928Z', + method: 'GET', + url: 'https://unpkg.com/director@1.2.8/build/director.js', + status: 200, + mimeType: 'application/x-javascript', + requestSentTime: 18098833.537, + requestStartTime: 18098837.233999997, + loadEndTime: 18098977.648000002, + timings: { + blocked: 84.54599999822676, + receive: 3.068000001803739, + queueing: 3.69700000010198, + proxy: -1, + total: 144.1110000014305, + wait: 52.56100000042352, + connect: -1, + send: 0.2390000008745119, + ssl: -1, + dns: -1, + }, + }, +]; + describe('Palettes', () => { it('A colour palette comprising timing and mime type colours is correctly generated', () => { expect(colourPalette).toEqual({ @@ -30,139 +163,6 @@ describe('Palettes', () => { }); describe('getSeriesAndDomain', () => { - const networkItems: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/todomvc-app-css@2.0.4/index.css', - status: 200, - mimeType: 'text/css', - requestSentTime: 18098833.175, - requestStartTime: 18098835.439, - loadEndTime: 18098957.145, - timings: { - connect: 81.10800000213203, - wait: 34.577999998873565, - receive: 0.5520000013348181, - send: 0.3600000018195715, - total: 123.97000000055414, - proxy: -1, - blocked: 0.8540000017092098, - queueing: 2.263999998831423, - ssl: 55.38700000033714, - dns: 3.559999997378327, - }, - }, - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - - const networkItemsWithoutFullTimings: NetworkItems = [ - networkItems[0], - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: 2.7929999996558763, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutAnyTimings: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - timings: { - total: -1, - blocked: -1, - ssl: -1, - wait: -1, - connect: -1, - dns: -1, - queueing: -1, - send: -1, - proxy: -1, - receive: -1, - }, - }, - ]; - - const networkItemsWithoutTimingsObject: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'file:///Users/dominiqueclarke/dev/synthetics/examples/todos/app/app.js', - status: 0, - mimeType: 'text/javascript', - requestSentTime: 18098834.097, - loadEndTime: 18098836.889999997, - }, - ]; - - const networkItemsWithUncommonMimeType: NetworkItems = [ - { - timestamp: '2021-01-05T19:22:28.928Z', - method: 'GET', - url: 'https://unpkg.com/director@1.2.8/build/director.js', - status: 200, - mimeType: 'application/x-javascript', - requestSentTime: 18098833.537, - requestStartTime: 18098837.233999997, - loadEndTime: 18098977.648000002, - timings: { - blocked: 84.54599999822676, - receive: 3.068000001803739, - queueing: 3.69700000010198, - proxy: -1, - total: 144.1110000014305, - wait: 52.56100000042352, - connect: -1, - send: 0.2390000008745119, - ssl: -1, - dns: -1, - }, - }, - ]; - it('formats timings', () => { const actual = getSeriesAndDomain(networkItems); expect(actual).toMatchInlineSnapshot(` @@ -175,6 +175,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -188,6 +189,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -201,6 +203,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -214,6 +217,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -227,6 +231,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -240,6 +245,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -253,6 +259,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -266,6 +273,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -279,6 +287,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -292,6 +301,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -305,6 +315,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -316,6 +327,7 @@ describe('getSeriesAndDomain', () => { "y0": 137.70799999925657, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -332,6 +344,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#dcd4c4", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#dcd4c4", @@ -345,6 +358,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#54b399", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#54b399", @@ -358,6 +372,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#da8b45", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#da8b45", @@ -371,6 +386,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#edc5a2", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#edc5a2", @@ -384,6 +400,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#d36086", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#d36086", @@ -397,6 +414,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#b0c9e0", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#b0c9e0", @@ -410,6 +428,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#ca8eae", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#ca8eae", @@ -423,6 +442,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "#9170b8", + "isHighlighted": true, "showTooltip": true, "tooltipProps": Object { "colour": "#9170b8", @@ -434,6 +454,7 @@ describe('getSeriesAndDomain', () => { "y0": 0.9219999983906746, }, ], + "totalHighlightedRequests": 2, } `); }); @@ -450,6 +471,7 @@ describe('getSeriesAndDomain', () => { Object { "config": Object { "colour": "", + "isHighlighted": true, "showTooltip": false, "tooltipProps": undefined, }, @@ -458,6 +480,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -473,6 +496,7 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { + "isHighlighted": true, "showTooltip": false, }, "x": 0, @@ -480,6 +504,7 @@ describe('getSeriesAndDomain', () => { "y0": 0, }, ], + "totalHighlightedRequests": 1, } `); }); @@ -501,4 +526,41 @@ describe('getSeriesAndDomain', () => { }); expect(contentDownloadedingConfigItem).toBeDefined(); }); + + it('counts the total number of highlighted items', () => { + // only one CSS file in this array of network Items + const actual = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + expect(actual.totalHighlightedRequests).toBe(1); + }); + + it('adds isHighlighted to waterfall entry when filter matches', () => { + // only one CSS file in this array of network Items + const { series } = getSeriesAndDomain(networkItems, false, '', ['stylesheet']); + series.forEach((item) => { + if (item.x === 0) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); + + it('adds isHighlighted to waterfall entry when query matches', () => { + // only the second item matches this query + const { series } = getSeriesAndDomain(networkItems, false, 'director', []); + series.forEach((item) => { + if (item.x === 1) { + expect(item.config.isHighlighted).toBe(true); + } else { + expect(item.config.isHighlighted).toBe(false); + } + }); + }); +}); + +describe('getSidebarItems', () => { + it('passes the item index offset by 1 to offsetIndex for visual display', () => { + const actual = getSidebarItems(networkItems, false, '', []); + expect(actual[0].offsetIndex).toBe(1); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 0ac93794594c0..46f0d23d0a6b9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -55,8 +55,28 @@ const getFriendlyTooltipValue = ({ } return `${label}: ${formatValueForDisplay(value)}ms`; }; +export const isHighlightedItem = ( + item: NetworkItem, + query?: string, + activeFilters: string[] = [] +) => { + if (!query && activeFilters?.length === 0) { + return true; + } + + const matchQuery = query ? item.url?.includes(query) : true; + const matchFilters = + activeFilters.length > 0 ? activeFilters.includes(MimeTypesMap[item.mimeType!]) : true; + + return !!(matchQuery && matchFilters); +}; -export const getSeriesAndDomain = (items: NetworkItems) => { +export const getSeriesAndDomain = ( + items: NetworkItems, + onlyHighlighted = false, + query?: string, + activeFilters?: string[] +) => { const getValueForOffset = (item: NetworkItem) => { return item.requestSentTime; }; @@ -78,13 +98,21 @@ export const getSeriesAndDomain = (items: NetworkItems) => { } }; + let totalHighlightedRequests = 0; + const series = items.reduce((acc, item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + if (isHighlighted) { + totalHighlightedRequests++; + } + if (!item.timings) { acc.push({ x: index, y0: 0, y: 0, config: { + isHighlighted, showTooltip: false, }, }); @@ -96,10 +124,13 @@ export const getSeriesAndDomain = (items: NetworkItems) => { let currentOffset = offsetValue - zeroOffset; + let timingValueFound = false; + TIMING_ORDER.forEach((timing) => { const value = getValue(item.timings, timing); - const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; if (value && value >= 0) { + timingValueFound = true; + const colour = timing === Timings.Receive ? mimeTypeColour : colourPalette[timing]; const y = currentOffset + value; acc.push({ @@ -108,6 +139,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y, config: { colour, + isHighlighted, showTooltip: true, tooltipProps: { value: getFriendlyTooltipValue({ @@ -126,7 +158,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { /* if no specific timing values are found, use the total time * if total time is not available use 0, set showTooltip to false, * and omit tooltip props */ - if (!acc.find((entry) => entry.x === index)) { + if (!timingValueFound) { const total = item.timings.total; const hasTotal = total !== -1; acc.push({ @@ -134,6 +166,7 @@ export const getSeriesAndDomain = (items: NetworkItems) => { y0: hasTotal ? currentOffset : 0, y: hasTotal ? currentOffset + item.timings.total : 0, config: { + isHighlighted, colour: hasTotal ? mimeTypeColour : '', showTooltip: hasTotal, tooltipProps: hasTotal @@ -154,14 +187,31 @@ export const getSeriesAndDomain = (items: NetworkItems) => { const yValues = series.map((serie) => serie.y); const domain = { min: 0, max: Math.max(...yValues) }; - return { series, domain }; + + let filteredSeries = series; + if (onlyHighlighted) { + filteredSeries = series.filter((item) => item.config.isHighlighted); + } + + return { series: filteredSeries, domain, totalHighlightedRequests }; }; -export const getSidebarItems = (items: NetworkItems): SidebarItems => { - return items.map((item) => { +export const getSidebarItems = ( + items: NetworkItems, + onlyHighlighted: boolean, + query: string, + activeFilters: string[] +): SidebarItems => { + const sideBarItems = items.map((item, index) => { + const isHighlighted = isHighlightedItem(item, query, activeFilters); + const offsetIndex = index + 1; const { url, status, method } = item; - return { url, status, method }; + return { url, status, method, isHighlighted, offsetIndex }; }); + if (onlyHighlighted) { + return sideBarItems.filter((item) => item.isHighlighted); + } + return sideBarItems; }; export const getLegendItems = (): LegendItems => { @@ -184,6 +234,7 @@ export const getLegendItems = (): LegendItems => { { name: FriendlyMimetypeLabels[mimeType], colour: MIME_TYPE_PALETTE[mimeType] }, ]; }); + return [...timingItems, ...mimeTypeItems]; }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts index 8d261edc74bf4..e22caae0d9eb2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/types.ts @@ -61,16 +61,13 @@ export const TIMING_ORDER = [ Timings.Receive, ] as const; -export type CalculatedTimings = { - [K in Timings]?: number; -}; - export enum MimeType { Html = 'html', Script = 'script', Stylesheet = 'stylesheet', Media = 'media', Font = 'font', + XHR = 'xhr', Other = 'other', } @@ -99,6 +96,9 @@ export const FriendlyMimetypeLabels = { [MimeType.Font]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.font', { defaultMessage: 'Font', }), + [MimeType.XHR]: i18n.translate('xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.xhr', { + defaultMessage: 'XHR', + }), [MimeType.Other]: i18n.translate( 'xpack.uptime.synthetics.waterfallChart.labels.mimeTypes.other', { @@ -112,7 +112,6 @@ export const FriendlyMimetypeLabels = { export const MimeTypesMap: Record = { 'text/html': MimeType.Html, 'application/javascript': MimeType.Script, - 'application/json': MimeType.Script, 'text/javascript': MimeType.Script, 'text/css': MimeType.Stylesheet, // Images @@ -146,38 +145,18 @@ export const MimeTypesMap: Record = { 'application/font-woff2': MimeType.Font, 'application/vnd.ms-fontobject': MimeType.Font, 'application/font-sfnt': MimeType.Font, + + // XHR + 'application/json': MimeType.XHR, }; export type NetworkItem = NetworkEvent; export type NetworkItems = NetworkItem[]; -// NOTE: A number will always be present if the property exists, but that number might be -1, which represents no value. -export interface PayloadTimings { - dns_start: number; - push_end: number; - worker_fetch_start: number; - worker_respond_with_settled: number; - proxy_end: number; - worker_start: number; - worker_ready: number; - send_end: number; - connect_end: number; - connect_start: number; - send_start: number; - proxy_start: number; - push_start: number; - ssl_end: number; - receive_headers_end: number; - ssl_start: number; - request_time: number; - dns_end: number; -} - -export interface ExtraSeriesConfig { - colour: string; -} - -export type SidebarItem = Pick; +export type SidebarItem = Pick & { + isHighlighted: boolean; + offsetIndex: number; +}; export type SidebarItems = SidebarItem[]; export interface LegendItem { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx new file mode 100644 index 0000000000000..e22f4a4c63f59 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { act, fireEvent } from '@testing-library/react'; +import { WaterfallChartWrapper } from './waterfall_chart_wrapper'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import { extractItems, isHighlightedItem } from './data_formatting'; + +import 'jest-canvas-mock'; +import { BAR_HEIGHT } from '../../waterfall/components/constants'; +import { MimeType } from './types'; +import { + FILTER_POPOVER_OPEN_LABEL, + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; + +const getHighLightedItems = (query: string, filters: string[]) => { + return NETWORK_EVENTS.events.filter((item) => isHighlightedItem(item, query, filters)); +}; + +describe('waterfall chart wrapper', () => { + jest.useFakeTimers(); + + it('renders the correct sidebar items', () => { + const { getAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + }); + + it('search by query works', () => { + const { getAllByTestId, getByTestId, getByLabelText } = render( + + ); + + const filterInput = getByLabelText(FILTER_REQUESTS_LABEL); + + const searchText = '.js'; + + fireEvent.change(filterInput, { target: { value: searchText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems(searchText, []).length; + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + const SIDE_BAR_ITEMS_HEIGHT = NETWORK_EVENTS.events.length * BAR_HEIGHT; + expect(getByTestId('wfSidebarContainer')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + + expect(getByTestId('wfDataOnlyBarChart')).toHaveAttribute('height', `${SIDE_BAR_ITEMS_HEIGHT}`); + }); + + it('search by mime type works', () => { + const { getAllByTestId, getByLabelText, getAllByText } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('XHR')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.XHR]).length; + + expect(getAllByTestId('sideBarHighlightedItem')).toHaveLength(highlightedItemsLength); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + }); + + it('renders sidebar even when filter matches 0 resources', () => { + const { getAllByTestId, getByLabelText, getAllByText, queryAllByTestId } = render( + + ); + + const sideBarItems = getAllByTestId('middleTruncatedTextSROnly'); + + expect(sideBarItems).toHaveLength(5); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getAllByText('CSS')[1]); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const highlightedItemsLength = getHighLightedItems('', [MimeType.Stylesheet]).length; + + // no CSS items found + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(getAllByTestId('sideBarDimmedItem')).toHaveLength( + NETWORK_EVENTS.events.length - highlightedItemsLength + ); + + fireEvent.click(getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL)); + + // filter bar is still accessible even when no resources match filter + expect(getByLabelText(FILTER_REQUESTS_LABEL)).toBeInTheDocument(); + + // no resources items are in the chart as none match filter + expect(queryAllByTestId('sideBarHighlightedItem')).toHaveLength(0); + expect(queryAllByTestId('sideBarDimmedItem')).toHaveLength(0); + }); +}); + +const NETWORK_EVENTS = { + events: [ + { + timestamp: '2021-01-21T10:31:21.537Z', + method: 'GET', + url: + 'https://apv-static.minute.ly/videos/v-c2a526c7-450d-428e-1244649-a390-fb639ffead96-s45.746-54.421m.mp4', + status: 206, + mimeType: 'video/mp4', + requestSentTime: 241114127.474, + requestStartTime: 241114129.214, + loadEndTime: 241116573.402, + timings: { + total: 2445.928000001004, + queueing: 1.7399999778717756, + blocked: 0.391999987186864, + receive: 2283.964000031119, + connect: 91.5709999972023, + wait: 28.795999998692423, + proxy: -1, + dns: 36.952000024029985, + send: 0.10000000474974513, + ssl: 64.28900000173599, + }, + }, + { + timestamp: '2021-01-21T10:31:22.174Z', + method: 'GET', + url: 'https://dpm.demdex.net/ibs:dpid=73426&dpuuid=31597189268188866891125449924942215949', + status: 200, + mimeType: 'image/gif', + requestSentTime: 241114749.202, + requestStartTime: 241114750.426, + loadEndTime: 241114805.541, + timings: { + queueing: 1.2240000069141388, + receive: 2.218999987235293, + proxy: -1, + dns: -1, + send: 0.14200000441633165, + blocked: 1.033000007737428, + total: 56.33900000248104, + wait: 51.72099999617785, + ssl: -1, + connect: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.679Z', + method: 'GET', + url: 'https://dapi.cms.mlbinfra.com/v2/content/en-us/sel-t119-homepage-mediawall', + status: 200, + mimeType: 'application/json', + requestSentTime: 241114268.04299998, + requestStartTime: 241114270.184, + loadEndTime: 241114665.609, + timings: { + total: 397.5659999996424, + dns: 29.5429999823682, + wait: 221.6830000106711, + queueing: 2.1410000044852495, + connect: 106.95499999565072, + ssl: 69.06899999012239, + receive: 2.027999988058582, + blocked: 0.877000013133511, + send: 23.719999997410923, + proxy: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.runtime.b313577971db9c857801.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114303.84899998, + requestStartTime: 241114306.416, + loadEndTime: 241114370.361, + timings: { + send: 1.357000001007691, + wait: 40.12299998430535, + receive: 16.78500001435168, + ssl: -1, + queueing: 2.5670000177342445, + total: 66.51200001942925, + connect: -1, + blocked: 5.680000002030283, + proxy: -1, + dns: -1, + }, + }, + { + timestamp: '2021-01-21T10:31:21.740Z', + method: 'GET', + url: 'https://platform.twitter.com/embed/embed.modules.7a266e7acfd42f2581a5.js', + status: 200, + mimeType: 'application/javascript', + requestSentTime: 241114305.939, + requestStartTime: 241114310.393, + loadEndTime: 241114938.264, + timings: { + wait: 51.61500000394881, + dns: -1, + ssl: -1, + receive: 506.5750000067055, + proxy: -1, + connect: -1, + blocked: 69.51599998865277, + queueing: 4.453999979887158, + total: 632.324999984121, + send: 0.16500000492669642, + }, + }, + ], +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index 91657981e7f89..8a0e9729a635b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -5,44 +5,14 @@ * 2.0. */ -import React, { useMemo, useState } from 'react'; -import { EuiHealth, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import { EuiHealth } from '@elastic/eui'; +import { useTrackMetric, METRIC_TYPE } from '../../../../../../../observability/public'; import { getSeriesAndDomain, getSidebarItems, getLegendItems } from './data_formatting'; import { SidebarItem, LegendItem, NetworkItems } from './types'; -import { - WaterfallProvider, - WaterfallChart, - MiddleTruncatedText, - RenderItem, -} from '../../waterfall'; - -export const renderSidebarItem: RenderItem = (item, index) => { - const { status } = item; - - const isErrorStatusCode = (statusCode: number) => { - const is400 = statusCode >= 400 && statusCode <= 499; - const is500 = statusCode >= 500 && statusCode <= 599; - const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; - return is400 || is500 || isSpecific300; - }; - - return ( - <> - {!status || !isErrorStatusCode(status) ? ( - - ) : ( - - - - - - {status} - - - )} - - ); -}; +import { WaterfallProvider, WaterfallChart, RenderItem } from '../../waterfall'; +import { WaterfallFilter } from './waterfall_filter'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; export const renderLegendItem: RenderItem = (item) => { return {item.name}; @@ -54,23 +24,64 @@ interface Props { } export const WaterfallChartWrapper: React.FC = ({ data, total }) => { + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [networkData] = useState(data); - const { series, domain } = useMemo(() => { - return getSeriesAndDomain(networkData); - }, [networkData]); + const hasFilters = activeFilters.length > 0; + + const { series, domain, totalHighlightedRequests } = useMemo(() => { + return getSeriesAndDomain(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const sidebarItems = useMemo(() => { - return getSidebarItems(networkData); - }, [networkData]); + return getSidebarItems(networkData, onlyHighlighted, query, activeFilters); + }, [networkData, query, activeFilters, onlyHighlighted]); const legendItems = getLegendItems(); + const renderFilter = useCallback(() => { + return ( + + ); + }, [activeFilters, setActiveFilters, onlyHighlighted, setOnlyHighlighted, query, setQuery]); + + const renderSidebarItem: RenderItem = useCallback( + (item) => { + return ( + + ); + }, + [hasFilters, onlyHighlighted] + ); + + useTrackMetric({ app: 'uptime', metric: 'waterfall_chart_view', metricType: METRIC_TYPE.COUNT }); + useTrackMetric({ + app: 'uptime', + metric: 'waterfall_chart_view', + metricType: METRIC_TYPE.COUNT, + delay: 15000, + }); + return ( { @@ -81,10 +92,19 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { tickFormat={(d: number) => `${Number(d).toFixed(0)} ms`} domain={domain} barStyleAccessor={(datum) => { + if (!datum.datum.config.isHighlighted) { + return { + rect: { + fill: datum.datum.config.colour, + opacity: '0.1', + }, + }; + } return datum.datum.config.colour; }} renderSidebarItem={renderSidebarItem} renderLegendItem={renderLegendItem} + renderFilter={renderFilter} fullHeight={true} /> diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx new file mode 100644 index 0000000000000..3acf6a269fb38 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { act, fireEvent } from '@testing-library/react'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { MIME_FILTERS, WaterfallFilter } from './waterfall_filter'; +import { + FILTER_REQUESTS_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, + FILTER_POPOVER_OPEN_LABEL, +} from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + jest.useFakeTimers(); + + it('renders correctly', () => { + const { getByLabelText, getByTitle } = render( + + ); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + MIME_FILTERS.forEach((filter) => { + expect(getByTitle(filter.label)); + }); + }); + + it('filter icon changes color on active/inactive filters', () => { + const Component = () => { + const [activeFilters, setActiveFilters] = useState([]); + + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--primary' + ); + + // toggle it back to inactive + fireEvent.click(getByTitle('XHR')); + + expect(getByLabelText(FILTER_POPOVER_OPEN_LABEL)).toHaveAttribute( + 'class', + 'euiButtonIcon euiButtonIcon--text' + ); + }); + + it('search input is working properly', () => { + const setQuery = jest.fn(); + + const Component = () => { + return ( + + ); + }; + const { getByLabelText } = render(); + + const testText = 'js'; + + fireEvent.change(getByLabelText(FILTER_REQUESTS_LABEL), { target: { value: testText } }); + + // inout has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + expect(setQuery).toHaveBeenCalledWith(testText); + }); + + it('resets checkbox when filters are removed', () => { + const Component = () => { + const [onlyHighlighted, setOnlyHighlighted] = useState(false); + const [query, setQuery] = useState(''); + const [activeFilters, setActiveFilters] = useState([]); + return ( + + ); + }; + const { getByLabelText, getByTitle } = render(); + const input = getByLabelText(FILTER_REQUESTS_LABEL); + // apply filters + const testText = 'js'; + fireEvent.change(input, { target: { value: testText } }); + fireEvent.click(getByLabelText(FILTER_POPOVER_OPEN_LABEL)); + const filterGroupButton = getByTitle('XHR'); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + const collapseCheckbox = getByLabelText(FILTER_COLLAPSE_REQUESTS_LABEL) as HTMLInputElement; + expect(collapseCheckbox).not.toBeDisabled(); + fireEvent.click(collapseCheckbox); + expect(collapseCheckbox).toBeChecked(); + + // remove filters + fireEvent.change(input, { target: { value: '' } }); + fireEvent.click(filterGroupButton); + + // input has debounce effect so hence the timer + act(() => { + jest.advanceTimersByTime(300); + }); + + // expect the checkbox to reset to disabled and unchecked + expect(collapseCheckbox).not.toBeChecked(); + expect(collapseCheckbox).toBeDisabled(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx new file mode 100644 index 0000000000000..42c2df4553b4c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_filter.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { + EuiButtonIcon, + EuiCheckbox, + EuiFieldSearch, + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiSpacer, +} from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; +import { + FILTER_REQUESTS_LABEL, + FILTER_SCREENREADER_LABEL, + FILTER_REMOVE_SCREENREADER_LABEL, + FILTER_POPOVER_OPEN_LABEL, + FILTER_COLLAPSE_REQUESTS_LABEL, +} from '../../waterfall/components/translations'; +import { MimeType, FriendlyMimetypeLabels } from './types'; +import { METRIC_TYPE, useUiTracker } from '../../../../../../../observability/public'; + +interface Props { + query: string; + activeFilters: string[]; + setActiveFilters: Dispatch>; + setQuery: (val: string) => void; + onlyHighlighted: boolean; + setOnlyHighlighted: (val: boolean) => void; +} + +export const MIME_FILTERS = [ + { + label: FriendlyMimetypeLabels[MimeType.XHR], + mimeType: MimeType.XHR, + }, + { + label: FriendlyMimetypeLabels[MimeType.Html], + mimeType: MimeType.Html, + }, + { + label: FriendlyMimetypeLabels[MimeType.Script], + mimeType: MimeType.Script, + }, + { + label: FriendlyMimetypeLabels[MimeType.Stylesheet], + mimeType: MimeType.Stylesheet, + }, + { + label: FriendlyMimetypeLabels[MimeType.Font], + mimeType: MimeType.Font, + }, + { + label: FriendlyMimetypeLabels[MimeType.Media], + mimeType: MimeType.Media, + }, +]; + +export const WaterfallFilter = ({ + query, + setQuery, + activeFilters, + setActiveFilters, + onlyHighlighted, + setOnlyHighlighted, +}: Props) => { + const [value, setValue] = useState(query); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const trackMetric = useUiTracker({ app: 'uptime' }); + + const toggleFilters = (val: string) => { + setActiveFilters((prevState) => + prevState.includes(val) ? prevState.filter((filter) => filter !== val) : [...prevState, val] + ); + }; + useDebounce( + () => { + setQuery(value); + }, + 250, + [value] + ); + + /* reset checkbox when there is no query or active filters + * this prevents the checkbox from being checked in a disabled state */ + useEffect(() => { + if (!(query || activeFilters.length > 0)) { + setOnlyHighlighted(false); + } + }, [activeFilters.length, setOnlyHighlighted, query]); + + // indicates use of the query input box + useEffect(() => { + if (query) { + trackMetric({ metric: 'waterfall_filter_input_changed', metricType: METRIC_TYPE.CLICK }); + } + }, [query, trackMetric]); + + // indicates the collapse to show only highlighted checkbox has been clicked + useEffect(() => { + if (onlyHighlighted) { + trackMetric({ + metric: 'waterfall_filter_collapse_checked', + metricType: METRIC_TYPE.CLICK, + }); + } + }, [onlyHighlighted, trackMetric]); + + // indicates filters have been applied or changed + useEffect(() => { + if (activeFilters.length > 0) { + trackMetric({ + metric: `waterfall_filters_applied_changed`, + metricType: METRIC_TYPE.CLICK, + }); + } + }, [activeFilters, trackMetric]); + + return ( + + + { + setValue(evt.target.value); + }} + value={value} + /> + + + setIsPopoverOpen((prevState) => !prevState)} + color={activeFilters.length > 0 ? 'primary' : 'text'} + isSelected={activeFilters.length > 0} + /> + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="rightCenter" + > + + {MIME_FILTERS.map(({ label, mimeType }) => ( + toggleFilters(mimeType)} + key={label} + withNext={true} + aria-label={`${ + activeFilters.includes(mimeType) + ? FILTER_REMOVE_SCREENREADER_LABEL + : FILTER_SCREENREADER_LABEL + } ${label}`} + > + {label} + + ))} + + + 0)} + id="onlyHighlighted" + label={FILTER_COLLAPSE_REQUESTS_LABEL} + checked={onlyHighlighted} + onChange={(e) => { + setOnlyHighlighted(e.target.checked); + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx new file mode 100644 index 0000000000000..25b577ef9403a --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { SidebarItem } from '../waterfall/types'; +import { MiddleTruncatedText } from '../../waterfall'; +import { SideBarItemHighlighter } from '../../waterfall/components/styles'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +interface SidebarItemProps { + item: SidebarItem; + renderFilterScreenReaderText?: boolean; +} + +export const WaterfallSidebarItem = ({ item, renderFilterScreenReaderText }: SidebarItemProps) => { + const { status, offsetIndex, isHighlighted } = item; + + const isErrorStatusCode = (statusCode: number) => { + const is400 = statusCode >= 400 && statusCode <= 499; + const is500 = statusCode >= 500 && statusCode <= 599; + const isSpecific300 = statusCode === 301 || statusCode === 307 || statusCode === 308; + return is400 || is500 || isSpecific300; + }; + + const text = `${offsetIndex}. ${item.url}`; + const ariaLabel = `${ + isHighlighted && renderFilterScreenReaderText + ? `${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ` + : '' + }${text}`; + + return ( + + {!status || !isErrorStatusCode(status) ? ( + + ) : ( + + + + + + {status} + + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx new file mode 100644 index 0000000000000..578d66a1ea3f1 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SidebarItem } from '../waterfall/types'; + +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; +import { WaterfallSidebarItem } from './waterfall_sidebar_item'; +import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; + +describe('waterfall filter', () => { + const url = 'http://www.elastic.co'; + const offsetIndex = 1; + const item: SidebarItem = { + url, + isHighlighted: true, + offsetIndex, + }; + + it('renders sidbar item', () => { + const { getByText } = render(); + + expect(getByText(`${offsetIndex}. ${url}`)); + }); + + it('render screen reader text when renderFilterScreenReaderText is true', () => { + const { getByLabelText } = render( + + ); + + expect( + getByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).toBeInTheDocument(); + }); + + it('does not render screen reader text when renderFilterScreenReaderText is false', () => { + const { queryByLabelText } = render( + + ); + + expect( + queryByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + ).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts index 543d6004b8955..a4b75174543a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/constants.ts @@ -17,3 +17,5 @@ export const FIXED_AXIS_HEIGHT = 32; // number of items to display in canvas, since canvas can only have limited size export const CANVAS_MAX_ITEMS = 150; + +export const CHART_LEGEND_PADDING = 62; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx index 9a3d4efb63a3a..d6c1d777a40a7 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -25,15 +25,21 @@ describe('getChunks', () => { }); describe('Component', () => { - it('renders truncated text', () => { - const { getByText } = render(); + it('renders truncated text and aria label', () => { + const { getByText, getByLabelText } = render( + + ); expect(getByText(first)).toBeInTheDocument(); expect(getByText(last)).toBeInTheDocument(); + + expect(getByLabelText(longString)).toBeInTheDocument(); }); it('renders screen reader only text', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const { getByText } = within(getByTestId('middleTruncatedTextSROnly')); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx index 9c263312f78f5..ec363ed2b40a4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -10,6 +10,11 @@ import styled from 'styled-components'; import { EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui'; import { FIXED_AXIS_HEIGHT } from './constants'; +interface Props { + ariaLabel: string; + text: string; +} + const OuterContainer = styled.div` width: 100%; height: 100%; @@ -50,14 +55,14 @@ export const getChunks = (text: string) => { // Helper component for adding middle text truncation, e.g. // really-really-really-long....ompressed.js // Can be used to accomodate content in sidebar item rendering. -export const MiddleTruncatedText = ({ text }: { text: string }) => { +export const MiddleTruncatedText = ({ ariaLabel, text }: Props) => { const chunks = useMemo(() => { return getChunks(text); }, [text]); return ( <> - + {text} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx index f46bab8c33a85..63b4d2945a51c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.test.tsx @@ -12,7 +12,11 @@ import { render } from '../../../../../lib/helper/rtl_helpers'; describe('NetworkRequestsTotal', () => { it('message in case total is greater than fetched', () => { const { getByText, getByLabelText } = render( - + ); expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); @@ -21,9 +25,52 @@ describe('NetworkRequestsTotal', () => { it('message in case total is equal to fetched requests', () => { const { getByText } = render( - + ); expect(getByText('500 network requests')).toBeInTheDocument(); }); + + it('does not show highlighted item message when showHighlightedNetworkEvents is false', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('does not show highlighted item message when highlightedNetworkEvents is less than 0', () => { + const { queryByText } = render( + + ); + + expect(queryByText(/match the filter/)).not.toBeInTheDocument(); + }); + + it('show highlighted item message when highlightedNetworkEvents is greater than 0 and showHighlightedNetworkEvents is true', () => { + const { getByText } = render( + + ); + + expect(getByText(/\(20 match the filter\)/)).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx index fce86c6b5c29d..5ccd60b0ce7a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiIconTip } from '@elastic/eui'; import { NetworkRequestsTotalStyle } from './styles'; @@ -13,24 +14,44 @@ import { NetworkRequestsTotalStyle } from './styles'; interface Props { totalNetworkRequests: number; fetchedNetworkRequests: number; + highlightedNetworkRequests: number; + showHighlightedNetworkRequests?: boolean; } -export const NetworkRequestsTotal = ({ totalNetworkRequests, fetchedNetworkRequests }: Props) => { +export const NetworkRequestsTotal = ({ + totalNetworkRequests, + fetchedNetworkRequests, + highlightedNetworkRequests, + showHighlightedNetworkRequests, +}: Props) => { return ( - {i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage', { - defaultMessage: '{numNetworkRequests} network requests', - values: { + fetchedNetworkRequests - ? i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage.first', { - defaultMessage: 'First {count}', - values: { count: `${fetchedNetworkRequests}/${totalNetworkRequests}` }, - }) - : totalNetworkRequests, - }, - })} + totalNetworkRequests > fetchedNetworkRequests ? ( + + ) : ( + totalNetworkRequests + ), + }} + />{' '} + {showHighlightedNetworkRequests && highlightedNetworkRequests >= 0 && ( + + )} {totalNetworkRequests > fetchedNetworkRequests && ( = ({ items, render }) => { return ( - + - {items.map((item, index) => { - return ( - - {render(item, index)} - - ); - })} + {items.map((item) => ( + + {render(item)} + + ))} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 333acd6e043df..c00c04b114045 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -14,10 +14,7 @@ interface WaterfallChartOuterContainerProps { height?: string; } -export const WaterfallChartOuterContainer = euiStyled.div` - height: ${(props) => (props.height ? `${props.height}` : 'auto')}; - overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; - overflow-x: hidden; +const StyledScrollDiv = euiStyled.div` &::-webkit-scrollbar { height: ${({ theme }) => theme.eui.euiScrollBar}; width: ${({ theme }) => theme.eui.euiScrollBar}; @@ -33,11 +30,27 @@ export const WaterfallChartOuterContainer = euiStyled.div` + height: ${(props) => (props.height ? `${props.height}` : 'auto')}; + overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; + overflow-x: hidden; +`; + +export const WaterfallChartFixedTopContainer = euiStyled(StyledScrollDiv)` position: sticky; top: 0; z-index: ${(props) => props.theme.eui.euiZLevel4}; - border-bottom: ${(props) => `1px solid ${props.theme.eui.euiColorLightShade}`}; + overflow-y: scroll; + overflow-x: hidden; +`; + +export const WaterfallChartAxisOnlyContainer = euiStyled(EuiFlexItem)` + margin-left: -22px; +`; + +export const WaterfallChartTopContainer = euiStyled(EuiFlexGroup)` `; export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` @@ -46,9 +59,18 @@ export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` border: none; `; // NOTE: border-radius !important is here as the "border" prop isn't working +export const WaterfallChartFilterContainer = euiStyled.div` + && { + padding: 16px; + z-index: ${(props) => props.theme.eui.euiZLevel5}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + } +`; // NOTE: border-radius !important is here as the "border" prop isn't working + export const WaterfallChartFixedAxisContainer = euiStyled.div` height: ${FIXED_AXIS_HEIGHT}px; z-index: ${(props) => props.theme.eui.euiZLevel4}; + height: 100%; `; interface WaterfallChartSidebarContainer { @@ -74,6 +96,12 @@ export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` min-width: 0; padding-left: ${(props) => props.theme.eui.paddingSizes.m}; padding-right: ${(props) => props.theme.eui.paddingSizes.m}; + z-index: ${(props) => props.theme.eui.euiZLevel4}; +`; + +export const SideBarItemHighlighter = euiStyled.span<{ isHighlighted: boolean }>` + opacity: ${(props) => (props.isHighlighted ? 1 : 0.4)}; + height: 100%; `; interface WaterfallChartChartContainer { @@ -106,6 +134,12 @@ export const WaterfallChartTooltip = euiStyled.div` `; export const NetworkRequestsTotalStyle = euiStyled(EuiText)` - line-height: ${FIXED_AXIS_HEIGHT}px; - margin-left: ${(props) => props.theme.eui.paddingSizes.m} + line-height: 28px; + padding: 0 ${(props) => props.theme.eui.paddingSizes.m}; + border-bottom: 0.3px solid ${(props) => props.theme.eui.euiColorLightShade}; + z-index: ${(props) => props.theme.eui.euiZLevel5}; +`; + +export const RelativeContainer = euiStyled.div` + position: relative; `; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts new file mode 100644 index 0000000000000..b63ffacaadd2e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/translations.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const FILTER_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.searchBox.placeholder', + { + defaultMessage: 'Filter network requests', + } +); + +export const FILTER_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.filterScreenreaderLabel', + { + defaultMessage: 'Filter by', + } +); + +export const FILTER_REMOVE_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.filterGroup.removeFilterScreenReaderLabel', + { + defaultMessage: 'Remove filter by', + } +); + +export const FILTER_POPOVER_OPEN_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.popover', + { + defaultMessage: 'Click to open waterfall filters', + } +); + +export const FILTER_COLLAPSE_REQUESTS_LABEL = i18n.translate( + 'xpack.uptime.pingList.synthetics.waterfall.filters.collapseRequestsLabel', + { + defaultMessage: 'Collapse to only show matching requests', + } +); + +export const SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL = i18n.translate( + 'xpack.uptime.synthetics.waterfall.sidebar.filterMatchesScreenReaderLabel', + { + defaultMessage: 'Resource matches filter', + } +); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx index 1ce46fc0d6e7b..a963fb1e2939c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.test.tsx @@ -10,9 +10,14 @@ import { renderHook } from '@testing-library/react-hooks'; import { IWaterfallContext } from '../context/waterfall_chart'; import { CANVAS_MAX_ITEMS } from './constants'; -const generateTestData = (): IWaterfallContext['data'] => { +const generateTestData = ( + { + xMultiplier, + }: { + xMultiplier: number; + } = { xMultiplier: 1 } +): IWaterfallContext['data'] => { const numberOfItems = 1000; - const data: IWaterfallContext['data'] = []; const testItem = { x: 0, @@ -29,11 +34,11 @@ const generateTestData = (): IWaterfallContext['data'] => { data.push( { ...testItem, - x: i, + x: xMultiplier * i, }, { ...testItem, - x: i, + x: xMultiplier * i, y0: 7, y: 25, } @@ -44,7 +49,7 @@ const generateTestData = (): IWaterfallContext['data'] => { }; describe('useBarChartsHooks', () => { - it('returns result as expected', () => { + it('returns result as expected for non filtered data', () => { const { result, rerender } = renderHook((props) => useBarCharts(props), { initialProps: { data: [] as IWaterfallContext['data'] }, }); @@ -70,4 +75,35 @@ describe('useBarChartsHooks', () => { expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4); expect(lastChartItems[lastChartItems.length - 1].x).toBe(CANVAS_MAX_ITEMS * 5 - 1); }); + + it('returns result as expected for filtered data', () => { + /* multiply x values to simulate filtered data, where x values can have gaps in the + * sequential order */ + const xMultiplier = 2; + const { result, rerender } = renderHook((props) => useBarCharts(props), { + initialProps: { data: [] as IWaterfallContext['data'] }, + }); + + expect(result.current).toHaveLength(0); + const newData = generateTestData({ xMultiplier }); + + rerender({ data: newData }); + + // Thousands items will result in 7 Canvas + expect(result.current.length).toBe(7); + + const firstChartItems = result.current[0]; + const lastChartItems = result.current[4]; + + // first chart items last item should be x 149, since we only display 150 items + expect(firstChartItems[firstChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS - 1) * xMultiplier + ); + + // since here are 5 charts, last chart first item should be x 600 + expect(lastChartItems[0].x).toBe(CANVAS_MAX_ITEMS * 4 * xMultiplier); + expect(lastChartItems[lastChartItems.length - 1].x).toBe( + (CANVAS_MAX_ITEMS * 5 - 1) * xMultiplier + ); + }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts index 79fd437039afe..2baf895504911 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/use_bar_charts.ts @@ -13,27 +13,36 @@ export interface UseBarHookProps { data: IWaterfallContext['data']; } -export const useBarCharts = ({ data = [] }: UseBarHookProps) => { +export const useBarCharts = ({ data }: UseBarHookProps) => { const [charts, setCharts] = useState>([]); useEffect(() => { - if (data.length > 0) { - let chartIndex = 0; - - const chartsN: Array = []; + const chartsN: Array = []; + if (data?.length > 0) { + let chartIndex = 0; + /* We want at most CANVAS_MAX_ITEMS **RESOURCES** per array. + * Resources !== individual timing items, but are comprised of many individual timing + * items. The X value of each item can be used as an id for the resource. + * We must keep track of the number of unique resources added to the each array. */ + const uniqueResources = new Set(); + let lastIndex: number; data.forEach((item) => { - // Subtract 1 to account for x value starting from 0 - if (item.x === CANVAS_MAX_ITEMS * chartIndex && !chartsN[item.x / CANVAS_MAX_ITEMS]) { - chartsN.push([item]); + if (uniqueResources.size === CANVAS_MAX_ITEMS && item.x > lastIndex) { chartIndex++; + uniqueResources.clear(); + } + uniqueResources.add(item.x); + lastIndex = item.x; + if (!chartsN[chartIndex]) { + chartsN.push([item]); return; } - chartsN[chartIndex - 1].push(item); + chartsN[chartIndex].push(item); }); - - setCharts(chartsN); } + + setCharts(chartsN); }, [data]); return charts; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx index 7c9051e8f6acf..528d749f576fc 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall.test.tsx @@ -6,64 +6,38 @@ */ import React from 'react'; -import { of } from 'rxjs'; -import { MountWithReduxProvider, mountWithRouter } from '../../../../../lib'; -import { KibanaContextProvider } from '../../../../../../../../../src/plugins/kibana_react/public'; import { WaterfallChart } from './waterfall_chart'; -import { - renderLegendItem, - renderSidebarItem, -} from '../../step_detail/waterfall/waterfall_chart_wrapper'; -import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { WaterfallChartOuterContainer } from './styles'; +import { renderLegendItem } from '../../step_detail/waterfall/waterfall_chart_wrapper'; +import { render } from '../../../../../lib/helper/rtl_helpers'; + +import 'jest-canvas-mock'; describe('waterfall', () => { it('sets the correct height in case of full height', () => { - const core = mockCore(); - const Component = () => { return ( - `${Number(d).toFixed(0)} ms`} - domain={{ - max: 3371, - min: 0, - }} - barStyleAccessor={(datum) => { - return datum.datum.config.colour; - }} - renderSidebarItem={renderSidebarItem} - renderLegendItem={renderLegendItem} - fullHeight={true} - /> +
+ `${Number(d).toFixed(0)} ms`} + domain={{ + max: 3371, + min: 0, + }} + barStyleAccessor={(datum) => { + return datum.datum.config.colour; + }} + renderSidebarItem={undefined} + renderLegendItem={renderLegendItem} + fullHeight={true} + /> +
); }; - const component = mountWithRouter( - - - - - - - - ); + const { getByTestId } = render(); - const chartWrapper = component.find(WaterfallChartOuterContainer); + const chartWrapper = getByTestId('waterfallOuterContainer'); - expect(chartWrapper.get(0).props.height).toBe('calc(100vh - 0px)'); + expect(chartWrapper).toHaveStyleRule('height', 'calc(100vh - 62px)'); }); }); - -const mockCore: () => any = () => { - return { - application: { - getUrlForApp: () => '/app/uptime', - navigateToUrl: jest.fn(), - }, - uiSettings: { - get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', - get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), - }, - }; -}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx new file mode 100644 index 0000000000000..df00df147fc6c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_bar_chart.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipInfo, +} from '@elastic/charts'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { BAR_HEIGHT } from './constants'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartChartContainer, WaterfallChartTooltip } from './styles'; +import { useWaterfallContext, WaterfallData } from '..'; + +const getChartHeight = (data: WaterfallData): number => { + // We get the last item x(number of bars) and adds 1 to cater for 0 index + const noOfXBars = new Set(data.map((item) => item.x)).size; + + return noOfXBars * BAR_HEIGHT; +}; + +const Tooltip = (tooltipInfo: TooltipInfo) => { + const { data, renderTooltipItem } = useWaterfallContext(); + const relevantItems = data.filter((item) => { + return ( + item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps + ); + }); + return relevantItems.length ? ( + + + {relevantItems.map((item, index) => { + return ( + {renderTooltipItem(item.config.tooltipProps)} + ); + })} + + + ) : null; +}; + +interface Props { + index: number; + chartData: WaterfallData; + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallBarChart = ({ + chartData, + tickFormat, + domain, + barStyleAccessor, + index, +}: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index 8f831d0629b25..e0e5165b41e49 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -5,62 +5,30 @@ * 2.0. */ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { - Axis, - BarSeries, - Chart, - Position, - ScaleType, - Settings, - TickFormatter, - DomainRange, - BarStyleAccessor, - TooltipInfo, - TooltipType, -} from '@elastic/charts'; -import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -// NOTE: The WaterfallChart has a hard requirement that consumers / solutions are making use of KibanaReactContext, and useKibana etc -// can therefore be accessed. -import { useUiSetting$ } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { TickFormatter, DomainRange, BarStyleAccessor } from '@elastic/charts'; + import { useWaterfallContext } from '../context/waterfall_chart'; import { WaterfallChartOuterContainer, WaterfallChartFixedTopContainer, WaterfallChartFixedTopContainerSidebarCover, - WaterfallChartFixedAxisContainer, - WaterfallChartChartContainer, - WaterfallChartTooltip, + WaterfallChartTopContainer, + RelativeContainer, + WaterfallChartFilterContainer, + WaterfallChartAxisOnlyContainer, } from './styles'; -import { WaterfallData } from '../types'; -import { BAR_HEIGHT, CANVAS_MAX_ITEMS, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; +import { CHART_LEGEND_PADDING, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from './constants'; import { Sidebar } from './sidebar'; import { Legend } from './legend'; import { useBarCharts } from './use_bar_charts'; +import { WaterfallBarChart } from './waterfall_bar_chart'; +import { WaterfallChartFixedAxis } from './waterfall_chart_fixed_axis'; import { NetworkRequestsTotal } from './network_requests_total'; -const Tooltip = (tooltipInfo: TooltipInfo) => { - const { data, renderTooltipItem } = useWaterfallContext(); - const relevantItems = data.filter((item) => { - return ( - item.x === tooltipInfo.header?.value && item.config.showTooltip && item.config.tooltipProps - ); - }); - return relevantItems.length ? ( - - - {relevantItems.map((item, index) => { - return ( - {renderTooltipItem(item.config.tooltipProps)} - ); - })} - - - ) : null; -}; - -export type RenderItem = (item: I, index: number) => JSX.Element; +export type RenderItem = (item: I, index?: number) => JSX.Element; +export type RenderFilter = () => JSX.Element; export interface WaterfallChartProps { tickFormat: TickFormatter; @@ -68,159 +36,100 @@ export interface WaterfallChartProps { barStyleAccessor: BarStyleAccessor; renderSidebarItem?: RenderItem; renderLegendItem?: RenderItem; + renderFilter?: RenderFilter; maxHeight?: string; fullHeight?: boolean; } -const getChartHeight = (data: WaterfallData, ind: number): number => { - // We get the last item x(number of bars) and adds 1 to cater for 0 index - return (data[data.length - 1]?.x + 1 - ind * CANVAS_MAX_ITEMS) * BAR_HEIGHT; -}; - export const WaterfallChart = ({ tickFormat, domain, barStyleAccessor, renderSidebarItem, renderLegendItem, + renderFilter, maxHeight = '800px', fullHeight = false, }: WaterfallChartProps) => { const { data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, } = useWaterfallContext(); - const [darkMode] = useUiSetting$('theme:darkMode'); - - const theme = useMemo(() => { - return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; - }, [darkMode]); - const chartWrapperDivRef = useRef(null); const [height, setHeight] = useState(maxHeight); - const shouldRenderSidebar = !!(sidebarItems && sidebarItems.length > 0 && renderSidebarItem); + const shouldRenderSidebar = !!(sidebarItems && renderSidebarItem); const shouldRenderLegend = !!(legendItems && legendItems.length > 0 && renderLegendItem); useEffect(() => { if (fullHeight && chartWrapperDivRef.current) { const chartOffset = chartWrapperDivRef.current.getBoundingClientRect().top; - setHeight(`calc(100vh - ${chartOffset}px)`); + setHeight(`calc(100vh - ${chartOffset + CHART_LEGEND_PADDING}px)`); } }, [chartWrapperDivRef, fullHeight]); const chartsToDisplay = useBarCharts({ data }); return ( - - <> - - - {shouldRenderSidebar && ( - - - - - - )} - - - - - - - - - - + + + + {shouldRenderSidebar && ( + + + + {renderFilter && ( + {renderFilter()} + )} - - - + )} + + + + + + + + {shouldRenderSidebar && } - + + {chartsToDisplay.map((chartData, ind) => ( - - - - - - - - - + chartData={chartData} + domain={domain} + barStyleAccessor={barStyleAccessor} + tickFormat={tickFormat} + /> ))} - + - {shouldRenderLegend && } - - + + {shouldRenderLegend && } + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx new file mode 100644 index 0000000000000..3a7ab421b6277 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart_fixed_axis.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + Axis, + BarSeries, + BarStyleAccessor, + Chart, + DomainRange, + Position, + ScaleType, + Settings, + TickFormatter, + TooltipType, +} from '@elastic/charts'; +import { useChartTheme } from '../../../../../hooks/use_chart_theme'; +import { WaterfallChartFixedAxisContainer } from './styles'; + +interface Props { + tickFormat: TickFormatter; + domain: DomainRange; + barStyleAccessor: BarStyleAccessor; +} + +export const WaterfallChartFixedAxis = ({ tickFormat, domain, barStyleAccessor }: Props) => { + const theme = useChartTheme(); + + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx index 68d24514a37d3..9e87d69ce38a8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -7,12 +7,15 @@ import React, { createContext, useContext, Context } from 'react'; import { WaterfallData, WaterfallDataEntry } from '../types'; +import { SidebarItems } from '../../step_detail/waterfall/types'; export interface IWaterfallContext { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: WaterfallData; - sidebarItems?: unknown[]; + showOnlyHighlightedNetworkRequests: boolean; + sidebarItems?: SidebarItems; legendItems?: unknown[]; renderTooltipItem: ( item: WaterfallDataEntry['config']['tooltipProps'], @@ -24,8 +27,10 @@ export const WaterfallContext = createContext>({}); interface ProviderProps { totalNetworkRequests: number; + highlightedNetworkRequests: number; fetchedNetworkRequests: number; data: IWaterfallContext['data']; + showOnlyHighlightedNetworkRequests: IWaterfallContext['showOnlyHighlightedNetworkRequests']; sidebarItems?: IWaterfallContext['sidebarItems']; legendItems?: IWaterfallContext['legendItems']; renderTooltipItem: IWaterfallContext['renderTooltipItem']; @@ -34,20 +39,24 @@ interface ProviderProps { export const WaterfallProvider: React.FC = ({ children, data, + showOnlyHighlightedNetworkRequests, sidebarItems, legendItems, renderTooltipItem, totalNetworkRequests, + highlightedNetworkRequests, fetchedNetworkRequests, }) => { return ( diff --git a/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts b/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts new file mode 100644 index 0000000000000..f9231abaa75a8 --- /dev/null +++ b/x-pack/plugins/uptime/public/hooks/use_chart_theme.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; +import { useMemo } from 'react'; +import { useUiSetting$ } from '../../../../../src/plugins/kibana_react/public'; + +export const useChartTheme = () => { + const [darkMode] = useUiSetting$('theme:darkMode'); + + const theme = useMemo(() => { + return darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; + }, [darkMode]); + + return theme; +}; diff --git a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx index 9656c63274a13..4c81247fb2cf1 100644 --- a/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx @@ -8,10 +8,17 @@ import React, { ReactElement } from 'react'; import { Router } from 'react-router-dom'; import { MemoryHistory } from 'history/createMemoryHistory'; -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, History } from 'history'; import { mountWithIntl, renderWithIntl, shallowWithIntl } from '@kbn/test/jest'; import { MountWithReduxProvider } from './helper_with_redux'; import { AppState } from '../../state'; +import { mockState } from '../__mocks__/uptime_store.mock'; +import { KibanaProviderOptions, MockRouter } from './rtl_helpers'; + +interface RenderRouterOptions extends KibanaProviderOptions { + history?: History; + state?: Partial; +} const helperWithRouter: ( helper: (node: ReactElement) => R, @@ -67,3 +74,39 @@ export const mountWithRouterRedux = ( options?.storeState ); }; + +/* Custom enzyme render */ +export function render( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return renderWithIntl( + + + {ui} + + + ); +} + +/* Custom enzyme render */ +export function mount( + ui: ReactElement, + { history, core, kibanaProps, state }: RenderRouterOptions = {} +) { + const testState: AppState = { + ...mockState, + ...state, + }; + return mountWithIntl( + + + {ui} + + + ); +} diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index abc0451bf8efa..e02a2c6f9832f 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -6,6 +6,7 @@ */ import React, { ReactElement } from 'react'; +import { of } from 'rxjs'; import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; @@ -26,7 +27,7 @@ interface KibanaProps { services?: KibanaServices; } -interface KibanaProviderOptions { +export interface KibanaProviderOptions { core?: Partial & ExtraCore; kibanaProps?: KibanaProps; } @@ -54,6 +55,11 @@ const mockCore: () => any = () => { getUrlForApp: () => '/app/uptime', navigateToUrl: jest.fn(), }, + uiSettings: { + get: (key: string) => 'MMM D, YYYY @ HH:mm:ss.SSS', + get$: (key: string) => of('MMM D, YYYY @ HH:mm:ss.SSS'), + }, + usageCollection: { reportUiCounter: () => {} }, }; return core; From 31a3ec5934b0eb566d66797d43518933599fbfdc Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Mon, 8 Feb 2021 15:31:09 -0500 Subject: [PATCH 03/19] [Time To Visualize] Make State Transfer App Specific (#89804) * made state transfer app specific --- ...mbeddablestatetransfer.cleareditorstate.md | 11 ++- ...blestatetransfer.getincomingeditorstate.md | 5 +- ...tetransfer.getincomingembeddablepackage.md | 5 +- ...beddable-public.embeddablestatetransfer.md | 6 +- .../hooks/use_dashboard_container.ts | 6 +- .../embeddable_state_transfer.test.ts | 98 ++++++++++++++++--- .../embeddable_state_transfer.ts | 39 ++++++-- src/plugins/embeddable/public/public.api.md | 7 +- .../components/visualize_byvalue_editor.tsx | 2 +- .../components/visualize_editor.tsx | 4 +- .../components/visualize_listing.tsx | 2 +- .../application/utils/get_top_nav_config.tsx | 2 +- .../public/application/visualize_constants.ts | 1 + src/plugins/visualize/public/plugin.ts | 6 +- x-pack/plugins/lens/common/constants.ts | 1 + x-pack/plugins/lens/public/app_plugin/app.tsx | 4 +- .../lens/public/app_plugin/mounter.tsx | 4 +- x-pack/plugins/lens/public/plugin.ts | 4 +- x-pack/plugins/maps/public/render_app.tsx | 3 +- .../routes/list_page/load_list_and_render.tsx | 4 +- .../routes/map_page/saved_map/saved_map.ts | 4 +- 21 files changed, 162 insertions(+), 56 deletions(-) diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md index 5c1a6a0393c2e..034f9c70e389f 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md @@ -4,11 +4,20 @@ ## EmbeddableStateTransfer.clearEditorState() method +Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id + Signature: ```typescript -clearEditorState(): void; +clearEditorState(appId: string): void; ``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | + Returns: `void` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md index 1434de2c9870e..cd261bff5905b 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEditorState() method -Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage +Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id Signature: ```typescript -getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; +getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch incomingEditorState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md index 9ead71f0bb22c..47873c8e91e41 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md @@ -4,18 +4,19 @@ ## EmbeddableStateTransfer.getIncomingEmbeddablePackage() method -Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage +Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId Signature: ```typescript -getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; +getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | +| appId | string | The app to fetch EmbeddablePackageState for | | removeAfterFetch | boolean | Whether to remove the package state after fetch to prevent duplicates. | Returns: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md index 76b6708b93bd1..13c6c8c0325f1 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.md @@ -29,9 +29,9 @@ export declare class EmbeddableStateTransfer | Method | Modifiers | Description | | --- | --- | --- | -| [clearEditorState()](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | | -| [getIncomingEditorState(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [originating app](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) argument from the sessionStorage | -| [getIncomingEmbeddablePackage(removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) argument from the sessionStorage | +| [clearEditorState(appId)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.cleareditorstate.md) | | Clears the [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEditorState(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingeditorstate.md) | | Fetches an [editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) from the sessionStorage for the provided app id | +| [getIncomingEmbeddablePackage(appId, removeAfterFetch)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.getincomingembeddablepackage.md) | | Fetches an [embeddable package](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) from the sessionStorage for the given AppId | | [navigateToEditor(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetoeditor.md) | | A wrapper around the method which navigates to the specified appId with [embeddable editor state](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) | | [navigateToWithEmbeddablePackage(appId, options)](./kibana-plugin-plugins-embeddable-public.embeddablestatetransfer.navigatetowithembeddablepackage.md) | | A wrapper around the method which navigates to the specified appId with [embeddable package state](./kibana-plugin-plugins-embeddable-public.embeddablepackagestate.md) | diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts index b27322b6bec53..d12fea07bdd41 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts @@ -21,7 +21,7 @@ import { import { DashboardStateManager } from '../dashboard_state_manager'; import { getDashboardContainerInput, getSearchSessionIdFromURL } from '../dashboard_app_functions'; -import { DashboardContainer, DashboardContainerInput } from '../..'; +import { DashboardConstants, DashboardContainer, DashboardContainerInput } from '../..'; import { DashboardAppServices } from '../types'; import { DASHBOARD_CONTAINER_TYPE } from '..'; @@ -68,7 +68,9 @@ export const useDashboardContainer = ( searchSession.restore(searchSessionIdFromURL); } - const incomingEmbeddable = embeddable.getStateTransfer().getIncomingEmbeddablePackage(true); + const incomingEmbeddable = embeddable + .getStateTransfer() + .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, true); let canceled = false; let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined; diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts index 763186fc17c0c..a8ecb384f782b 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.test.ts @@ -42,6 +42,10 @@ describe('embeddable state transfer', () => { const destinationApp = 'superUltraVisualize'; const originatingApp = 'superUltraTestDashboard'; + const testAppId = 'testApp'; + + const buildKey = (appId: string, key: string) => `${appId}-${key}`; + beforeEach(() => { currentAppId$ = new Subject(); currentAppId$.next(originatingApp); @@ -82,7 +86,9 @@ describe('embeddable state transfer', () => { it('can send an outgoing editor state', async () => { await stateTransfer.navigateToEditor(destinationApp, { state: { originatingApp } }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -98,7 +104,9 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(destinationApp, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -117,7 +125,10 @@ describe('embeddable state transfer', () => { state: { type: 'coolestType', input: { savedObjectId: '150' } }, }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -133,7 +144,10 @@ describe('embeddable state transfer', () => { }); expect(store.set).toHaveBeenCalledWith(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { kibanaIsNowForSports: 'extremeSportsKibana', - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(destinationApp, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, }); expect(application.navigateToApp).toHaveBeenCalledWith('superUltraVisualize', { path: undefined, @@ -151,42 +165,92 @@ describe('embeddable state transfer', () => { it('can fetch an incoming editor state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, + }); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); + expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + }); + + it('can fetch an incoming editor state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey('otherApp1', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'whoops not me', + }, + [buildKey('otherApp2', EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'otherTestDashboard', + }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toEqual({ originatingApp: 'superUltraTestDashboard' }); + + const fetchedState2 = stateTransfer.getIncomingEditorState('otherApp2'); + expect(fetchedState2).toEqual({ originatingApp: 'otherTestDashboard' }); }); it('incoming editor state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { helloSportsKibana: 'superUltraTestDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + helloSportsKibana: 'superUltraTestDashboard', + }, }); - const fetchedState = stateTransfer.getIncomingEditorState(); + const fetchedState = stateTransfer.getIncomingEditorState(testAppId); expect(fetchedState).toBeUndefined(); }); it('can fetch an incoming embeddable package state', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'skisEmbeddable', input: { savedObjectId: '123' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); }); + it('can fetch an incoming embeddable package state and ignore state for other apps', async () => { + store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'skisEmbeddable', + input: { savedObjectId: '123' }, + }, + [buildKey('testApp2', EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }, + }); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); + expect(fetchedState).toEqual({ type: 'skisEmbeddable', input: { savedObjectId: '123' } }); + + const fetchedState2 = stateTransfer.getIncomingEmbeddablePackage('testApp2'); + expect(fetchedState2).toEqual({ + type: 'crossCountryEmbeddable', + input: { savedObjectId: '456' }, + }); + }); + it('embeddable package state returns undefined when state is not in the right shape', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { kibanaIsFor: 'sports' }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { kibanaIsFor: 'sports' }, }); - const fetchedState = stateTransfer.getIncomingEmbeddablePackage(); + const fetchedState = stateTransfer.getIncomingEmbeddablePackage(testAppId); expect(fetchedState).toBeUndefined(); }); it('removes embeddable package key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_PACKAGE_STATE_KEY]: { type: 'coolestType', input: { savedObjectId: '150' } }, + [buildKey(testAppId, EMBEDDABLE_PACKAGE_STATE_KEY)]: { + type: 'coolestType', + input: { savedObjectId: '150' }, + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEmbeddablePackage(true); + stateTransfer.getIncomingEmbeddablePackage(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); @@ -194,10 +258,12 @@ describe('embeddable state transfer', () => { it('removes editor state key when removeAfterFetch is true', async () => { store.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, { - [EMBEDDABLE_EDITOR_STATE_KEY]: { originatingApp: 'superCoolFootballDashboard' }, + [buildKey(testAppId, EMBEDDABLE_EDITOR_STATE_KEY)]: { + originatingApp: 'superCoolFootballDashboard', + }, iSHouldStillbeHere: 'doing the sports thing', }); - stateTransfer.getIncomingEditorState(true); + stateTransfer.getIncomingEditorState(testAppId, true); expect(store.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)).toEqual({ iSHouldStillbeHere: 'doing the sports thing', }); diff --git a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts index d3b1c1c76aadf..8664a5aae7345 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/embeddable_state_transfer.ts @@ -50,13 +50,18 @@ export class EmbeddableStateTransfer { public getAppNameFromId = (appId: string): string | undefined => this.appList?.get(appId)?.title; /** - * Fetches an {@link EmbeddableEditorState | originating app} argument from the sessionStorage + * Fetches an {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id * + * @param appId - The app to fetch incomingEditorState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ - public getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined { + public getIncomingEditorState( + appId: string, + removeAfterFetch?: boolean + ): EmbeddableEditorState | undefined { return this.getIncomingState( isEmbeddableEditorState, + appId, EMBEDDABLE_EDITOR_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_EDITOR_STATE_KEY] : undefined, @@ -64,24 +69,33 @@ export class EmbeddableStateTransfer { ); } - public clearEditorState() { + /** + * Clears the {@link EmbeddableEditorState | editor state} from the sessionStorage for the provided app id + * + * @param appId - The app to fetch incomingEditorState for + * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. + */ + public clearEditorState(appId: string) { const currentState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY); if (currentState) { - delete currentState[EMBEDDABLE_EDITOR_STATE_KEY]; + delete currentState[this.buildKey(appId, EMBEDDABLE_EDITOR_STATE_KEY)]; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, currentState); } } /** - * Fetches an {@link EmbeddablePackageState | embeddable package} argument from the sessionStorage + * Fetches an {@link EmbeddablePackageState | embeddable package} from the sessionStorage for the given AppId * + * @param appId - The app to fetch EmbeddablePackageState for * @param removeAfterFetch - Whether to remove the package state after fetch to prevent duplicates. */ public getIncomingEmbeddablePackage( + appId: string, removeAfterFetch?: boolean ): EmbeddablePackageState | undefined { return this.getIncomingState( isEmbeddablePackageState, + appId, EMBEDDABLE_PACKAGE_STATE_KEY, { keysToRemoveAfterFetch: removeAfterFetch ? [EMBEDDABLE_PACKAGE_STATE_KEY] : undefined, @@ -122,20 +136,27 @@ export class EmbeddableStateTransfer { }); } + private buildKey(appId: string, key: string) { + return `${appId}-${key}`; + } + private getIncomingState( guard: (state: unknown) => state is IncomingStateType, + appId: string, key: string, options?: { keysToRemoveAfterFetch?: string[]; } ): IncomingStateType | undefined { - const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[key]; + const incomingState = this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY)?.[ + this.buildKey(appId, key) + ]; const castState = !guard || guard(incomingState) ? (cloneDeep(incomingState) as IncomingStateType) : undefined; if (castState && options?.keysToRemoveAfterFetch) { const stateReplace = { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY) }; options.keysToRemoveAfterFetch.forEach((keyToRemove: string) => { - delete stateReplace[keyToRemove]; + delete stateReplace[this.buildKey(appId, keyToRemove)]; }); this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateReplace); } @@ -150,9 +171,9 @@ export class EmbeddableStateTransfer { const stateObject = options?.appendToExistingState ? { ...this.storage.get(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY), - [key]: options.state, + [this.buildKey(appId, key)]: options.state, } - : { [key]: options?.state }; + : { [this.buildKey(appId, key)]: options?.state }; this.storage.set(EMBEDDABLE_STATE_TRANSFER_STORAGE_KEY, stateObject); await this.navigateToApp(appId, { path: options?.path }); } diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 2f9b43121b45a..3e7014d54958d 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -590,11 +590,10 @@ export class EmbeddableStateTransfer { // Warning: (ae-forgotten-export) The symbol "ApplicationStart" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "PublicAppInfo" needs to be exported by the entry point index.d.ts constructor(navigateToApp: ApplicationStart['navigateToApp'], currentAppId$: ApplicationStart['currentAppId$'], appList?: ReadonlyMap | undefined, customStorage?: Storage); - // (undocumented) - clearEditorState(): void; + clearEditorState(appId: string): void; getAppNameFromId: (appId: string) => string | undefined; - getIncomingEditorState(removeAfterFetch?: boolean): EmbeddableEditorState | undefined; - getIncomingEmbeddablePackage(removeAfterFetch?: boolean): EmbeddablePackageState | undefined; + getIncomingEditorState(appId: string, removeAfterFetch?: boolean): EmbeddableEditorState | undefined; + getIncomingEmbeddablePackage(appId: string, removeAfterFetch?: boolean): EmbeddablePackageState | undefined; // (undocumented) isTransferInProgress: boolean; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ApplicationStart" diff --git a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx index 6ca6efaa89797..fa0e0bd5f48f0 100644 --- a/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_byvalue_editor.tsx @@ -34,7 +34,7 @@ export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { useEffect(() => { const { originatingApp: value, embeddableId: embeddableIdValue, valueInput: valueInputValue } = - services.stateTransferService.getIncomingEditorState() || {}; + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); setValueInput(valueInputValue); setEmbeddableId(embeddableIdValue); diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index 7465e7eaa9044..c6333e978183f 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -22,6 +22,7 @@ import { import { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; import { VisualizeAppProps } from '../app'; +import { VisualizeConstants } from '../..'; export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); @@ -54,7 +55,8 @@ export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { useLinkedSearchUpdates(services, eventEmitter, appState, savedVisInstance); useEffect(() => { - const { originatingApp: value } = services.stateTransferService.getIncomingEditorState() || {}; + const { originatingApp: value } = + services.stateTransferService.getIncomingEditorState(VisualizeConstants.APP_ID) || {}; setOriginatingApp(value); }, [services]); diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index c772554344cb2..bc766d63db5a7 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -65,7 +65,7 @@ export const VisualizeListing = () => { useMount(() => { // Reset editor state if the visualize listing page is loaded. - stateTransferService.clearEditorState(); + stateTransferService.clearEditorState(VisualizeConstants.APP_ID); chrome.setBreadcrumbs([ { text: i18n.translate('visualize.visualizeListingBreadcrumbsTitle', { diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 9ea42e8b56559..e8c3289d4ce41 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -142,7 +142,7 @@ export const getTopNavConfig = ( if (setOriginatingApp && originatingApp && newlyCreated) { setOriginatingApp(undefined); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(VisualizeConstants.APP_ID); } chrome.docTitle.change(savedVis.lastSavedTitle); chrome.setBreadcrumbs(getEditBreadcrumbs({}, savedVis.lastSavedTitle)); diff --git a/src/plugins/visualize/public/application/visualize_constants.ts b/src/plugins/visualize/public/application/visualize_constants.ts index 7dbf5be77b74d..6e901882a9365 100644 --- a/src/plugins/visualize/public/application/visualize_constants.ts +++ b/src/plugins/visualize/public/application/visualize_constants.ts @@ -16,4 +16,5 @@ export const VisualizeConstants = { CREATE_PATH: '/create', EDIT_PATH: '/edit', EDIT_BY_VALUE_PATH: '/edit_by_value', + APP_ID: 'visualize', }; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 3d82e6c60a1b6..4eb2d6fd2a731 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -132,7 +132,7 @@ export class VisualizePlugin setUISettings(core.uiSettings); core.application.register({ - id: 'visualize', + id: VisualizeConstants.APP_ID, title: 'Visualize', order: 8000, euiIconType: 'logoKibana', @@ -147,7 +147,9 @@ export class VisualizePlugin // allows the urlTracker to only save URLs that are not linked to an originatingApp this.isLinkedToOriginatingApp = () => { return Boolean( - pluginsStart.embeddable.getStateTransfer().getIncomingEditorState()?.originatingApp + pluginsStart.embeddable + .getStateTransfer() + .getIncomingEditorState(VisualizeConstants.APP_ID)?.originatingApp ); }; diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index 202b80d3d8406..c3e556b167889 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -6,6 +6,7 @@ */ export const PLUGIN_ID = 'lens'; +export const APP_ID = 'lens'; export const LENS_EMBEDDABLE_TYPE = 'lens'; export const DOC_TYPE = 'lens'; export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 7e95479887dbd..0d72a366fa411 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -38,7 +38,7 @@ import { SavedQuery, syncQueryStateWithUrl, } from '../../../../../src/plugins/data/public'; -import { LENS_EMBEDDABLE_TYPE, getFullPath } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common'; import { LensAppProps, LensAppServices, LensAppState } from './types'; import { getLensTopNavConfig } from './lens_top_nav'; import { Document } from '../persistence'; @@ -498,7 +498,7 @@ export function App({ isLinkedToOriginatingApp: false, })); // remove editor state so the connection is still broken after reload - stateTransfer.clearEditorState(); + stateTransfer.clearEditorState(APP_ID); redirectTo(newInput.savedObjectId); return; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 1ff31e5d4bf6b..5869151485a52 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -23,7 +23,7 @@ import { App } from './app'; import { EditorFrameStart } from '../types'; import { addHelpMenuToAppChrome } from '../help_menu_util'; import { LensPluginStartDependencies } from '../plugin'; -import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common'; import { LensEmbeddableInput, LensByReferenceInput, @@ -57,7 +57,7 @@ export async function mountApp( const storage = new Storage(localStorage); const stateTransfer = embeddable?.getStateTransfer(); const historyLocationState = params.history.location.state as HistoryLocationState; - const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(); + const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); const lensServices: LensAppServices = { data, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 05da76d9fd207..c667ddea06b33 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -40,7 +40,7 @@ import { ACTION_VISUALIZE_FIELD, VISUALIZE_FIELD_TRIGGER, } from '../../../../src/plugins/ui_actions/public'; -import { getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; +import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; import { visualizeFieldAction } from './trigger_actions/visualize_field_actions'; @@ -182,7 +182,7 @@ export class LensPlugin { }; core.application.register({ - id: 'lens', + id: APP_ID, title: NOT_INTERNATIONALIZED_PRODUCT_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index ccd30126b67bd..4d1dff9303b0c 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -26,6 +26,7 @@ import { } from '../../../../src/plugins/kibana_utils/public'; import { ListPage, MapPage } from './routes'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; +import { APP_ID } from '../common/constants'; export let goToSpecifiedPath: (path: string) => void; export let kbnUrlStateStorage: IKbnUrlStateStorage; @@ -80,7 +81,7 @@ export async function renderApp({ function renderMapApp(routeProps: RouteComponentProps<{ savedMapId?: string }>) { const { embeddableId, originatingApp, valueInput } = - stateTransfer.getIncomingEditorState() || {}; + stateTransfer.getIncomingEditorState(APP_ID) || {}; let mapEmbeddableInput; if (routeProps.match.params.savedMapId) { diff --git a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx index 66b65eb8d0a9d..feafb34f6a715 100644 --- a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; import { getSavedObjectsClient, getToasts } from '../../kibana_services'; import { MapsListView } from './maps_list_view'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; +import { APP_ID, MAP_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { EmbeddableStateTransfer } from '../../../../../../src/plugins/embeddable/public'; export class LoadListAndRender extends React.Component<{ stateTransfer: EmbeddableStateTransfer }> { @@ -22,7 +22,7 @@ export class LoadListAndRender extends React.Component<{ stateTransfer: Embeddab componentDidMount() { this._isMounted = true; - this.props.stateTransfer.clearEditorState(); + this.props.stateTransfer.clearEditorState(APP_ID); this._loadMapsList(); } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index d38ff8b3e4da6..b6ee5274f690d 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -9,7 +9,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; +import { APP_ID, MAP_PATH, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; import { createMapStore, MapStore, MapStoreState } from '../../../reducers/store'; import { getTimeFilters, @@ -364,7 +364,7 @@ export class SavedMap { this._originatingApp = undefined; // remove editor state so the connection is still broken after reload - this._getStateTransfer().clearEditorState(); + this._getStateTransfer().clearEditorState(APP_ID); getToasts().addSuccess({ title: i18n.translate('xpack.maps.topNav.saveSuccessMessage', { From cde3cbafe4f4506c7444b4c40a0a33027a4740e9 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 8 Feb 2021 15:53:01 -0500 Subject: [PATCH 04/19] fix summary alert details (#90657) --- .../factory/events/details/helpers.ts | 12 +- .../security_solution/timeline_details.ts | 158 +++++++++--------- 2 files changed, 82 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 4a6a1d61a9221..779454e9474ee 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -51,16 +51,8 @@ export const getDataFromSourceHits = ( { category: fieldCategory, field, - values: Array.isArray(item) - ? item.map((value) => { - if (isObject(value)) { - return JSON.stringify(value); - } - - return value; - }) - : [item], - originalValue: item, + values: toStringArray(item), + originalValue: toStringArray(item), } as TimelineEventsDetailsItem, ]; } else if (isObject(item)) { diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index 2705406009062..39b343a361945 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -19,97 +19,97 @@ const EXPECTED_DATA = [ category: 'base', field: '@timestamp', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: '@version', field: '@version', values: ['1'], - originalValue: '1', + originalValue: ['1'], }, { category: 'agent', field: 'agent.ephemeral_id', values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], - originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', + originalValue: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], }, { category: 'agent', field: 'agent.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'agent', field: 'agent.id', values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], - originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', + originalValue: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], }, { category: 'agent', field: 'agent.type', values: ['filebeat'], - originalValue: 'filebeat', + originalValue: ['filebeat'], }, { category: 'agent', field: 'agent.version', values: ['7.0.0'], - originalValue: '7.0.0', + originalValue: ['7.0.0'], }, { category: 'destination', field: 'destination.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'destination', field: 'destination.ip', values: ['10.100.7.196'], - originalValue: '10.100.7.196', + originalValue: ['10.100.7.196'], }, { category: 'destination', field: 'destination.port', - values: [40684], - originalValue: 40684, + values: ['40684'], + originalValue: ['40684'], }, { category: 'ecs', field: 'ecs.version', values: ['1.0.0-beta2'], - originalValue: '1.0.0-beta2', + originalValue: ['1.0.0-beta2'], }, { category: 'event', field: 'event.dataset', values: ['suricata.eve'], - originalValue: 'suricata.eve', + originalValue: ['suricata.eve'], }, { category: 'event', field: 'event.end', values: ['2019-02-10T02:39:44.107Z'], - originalValue: '2019-02-10T02:39:44.107Z', + originalValue: ['2019-02-10T02:39:44.107Z'], }, { category: 'event', field: 'event.kind', values: ['event'], - originalValue: 'event', + originalValue: ['event'], }, { category: 'event', field: 'event.module', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'event', field: 'event.type', values: ['fileinfo'], - originalValue: 'fileinfo', + originalValue: ['fileinfo'], }, { category: 'file', @@ -117,260 +117,261 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'file', field: 'file.size', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'fileset', field: 'fileset.name', values: ['eve'], - originalValue: 'eve', + originalValue: ['eve'], }, { category: 'flow', field: 'flow.locality', values: ['public'], - originalValue: 'public', + originalValue: ['public'], }, { category: 'host', field: 'host.architecture', values: ['armv7l'], - originalValue: 'armv7l', + originalValue: ['armv7l'], }, { category: 'host', field: 'host.hostname', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.id', values: ['b19a781f683541a7a25ee345133aa399'], - originalValue: 'b19a781f683541a7a25ee345133aa399', + originalValue: ['b19a781f683541a7a25ee345133aa399'], }, { category: 'host', field: 'host.name', values: ['raspberrypi'], - originalValue: 'raspberrypi', + originalValue: ['raspberrypi'], }, { category: 'host', field: 'host.os.codename', values: ['stretch'], - originalValue: 'stretch', + originalValue: ['stretch'], }, { category: 'host', field: 'host.os.family', values: [''], - originalValue: '', + originalValue: [''], }, { category: 'host', field: 'host.os.kernel', values: ['4.14.50-v7+'], - originalValue: '4.14.50-v7+', + originalValue: ['4.14.50-v7+'], }, { category: 'host', field: 'host.os.name', values: ['Raspbian GNU/Linux'], - originalValue: 'Raspbian GNU/Linux', + originalValue: ['Raspbian GNU/Linux'], }, { category: 'host', field: 'host.os.platform', values: ['raspbian'], - originalValue: 'raspbian', + originalValue: ['raspbian'], }, { category: 'host', field: 'host.os.version', values: ['9 (stretch)'], - originalValue: '9 (stretch)', + originalValue: ['9 (stretch)'], }, { category: 'http', field: 'http.request.method', values: ['get'], - originalValue: 'get', + originalValue: ['get'], }, { category: 'http', field: 'http.response.body.bytes', - values: [48277], - originalValue: 48277, + values: ['48277'], + originalValue: ['48277'], }, { category: 'http', field: 'http.response.status_code', - values: [206], - originalValue: 206, + values: ['206'], + originalValue: ['206'], }, { category: 'input', field: 'input.type', values: ['log'], - originalValue: 'log', + originalValue: ['log'], }, { category: 'base', field: 'labels.pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], - originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', + originalValue: ['filebeat-7.0.0-suricata-eve-pipeline'], }, { category: 'log', field: 'log.file.path', values: ['/var/log/suricata/eve.json'], - originalValue: '/var/log/suricata/eve.json', + originalValue: ['/var/log/suricata/eve.json'], }, { category: 'log', field: 'log.offset', - values: [1856288115], - originalValue: 1856288115, + values: ['1856288115'], + originalValue: ['1856288115'], }, { category: 'network', field: 'network.name', values: ['iot'], - originalValue: 'iot', + originalValue: ['iot'], }, { category: 'network', field: 'network.protocol', values: ['http'], - originalValue: 'http', + originalValue: ['http'], }, { category: 'network', field: 'network.transport', values: ['tcp'], - originalValue: 'tcp', + originalValue: ['tcp'], }, { category: 'service', field: 'service.type', values: ['suricata'], - originalValue: 'suricata', + originalValue: ['suricata'], }, { category: 'source', field: 'source.as.num', - values: [16509], - originalValue: 16509, + values: ['16509'], + originalValue: ['16509'], }, { category: 'source', field: 'source.as.org', values: ['Amazon.com, Inc.'], - originalValue: 'Amazon.com, Inc.', + originalValue: ['Amazon.com, Inc.'], }, { category: 'source', field: 'source.domain', values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], - originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', + originalValue: ['server-54-239-219-210.jfk51.r.cloudfront.net'], }, { category: 'source', field: 'source.geo.city_name', values: ['Seattle'], - originalValue: 'Seattle', + originalValue: ['Seattle'], }, { category: 'source', field: 'source.geo.continent_name', values: ['North America'], - originalValue: 'North America', + originalValue: ['North America'], }, { category: 'source', field: 'source.geo.country_iso_code', values: ['US'], - originalValue: 'US', + originalValue: ['US'], }, { category: 'source', field: 'source.geo.location.lat', - values: [47.6103], - originalValue: 47.6103, + values: ['47.6103'], + originalValue: ['47.6103'], }, { category: 'source', field: 'source.geo.location.lon', - values: [-122.3341], - originalValue: -122.3341, + values: ['-122.3341'], + originalValue: ['-122.3341'], }, { category: 'source', field: 'source.geo.region_iso_code', values: ['US-WA'], - originalValue: 'US-WA', + originalValue: ['US-WA'], }, { category: 'source', field: 'source.geo.region_name', values: ['Washington'], - originalValue: 'Washington', + originalValue: ['Washington'], }, { category: 'source', field: 'source.ip', values: ['54.239.219.210'], - originalValue: '54.239.219.210', + originalValue: ['54.239.219.210'], }, { category: 'source', field: 'source.port', - values: [80], - originalValue: 80, + values: ['80'], + originalValue: ['80'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.state', values: ['CLOSED'], - originalValue: 'CLOSED', + originalValue: ['CLOSED'], }, { category: 'suricata', field: 'suricata.eve.fileinfo.tx_id', - values: [301], - originalValue: 301, + values: ['301'], + originalValue: ['301'], }, { category: 'suricata', field: 'suricata.eve.flow_id', - values: [196625917175466], - originalValue: 196625917175466, + values: ['196625917175466'], + originalValue: ['196625917175466'], }, { category: 'suricata', field: 'suricata.eve.http.http_content_type', values: ['video/mp4'], - originalValue: 'video/mp4', + originalValue: ['video/mp4'], }, { category: 'suricata', field: 'suricata.eve.http.protocol', values: ['HTTP/1.1'], - originalValue: 'HTTP/1.1', + originalValue: ['HTTP/1.1'], }, { category: 'suricata', field: 'suricata.eve.in_iface', values: ['eth0'], - originalValue: 'eth0', + originalValue: ['eth0'], }, { category: 'base', @@ -382,7 +383,7 @@ const EXPECTED_DATA = [ category: 'url', field: 'url.domain', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', + originalValue: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], }, { category: 'url', @@ -390,8 +391,9 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: 'url', @@ -399,26 +401,27 @@ const EXPECTED_DATA = [ values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], - originalValue: + originalValue: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + ], }, { category: '_index', field: '_index', values: ['filebeat-7.0.0-iot-2019.06'], - originalValue: 'filebeat-7.0.0-iot-2019.06', + originalValue: ['filebeat-7.0.0-iot-2019.06'], }, { category: '_id', field: '_id', values: ['QRhG1WgBqd-n62SwZYDT'], - originalValue: 'QRhG1WgBqd-n62SwZYDT', + originalValue: ['QRhG1WgBqd-n62SwZYDT'], }, { category: '_score', field: '_score', - values: [1], - originalValue: 1, + values: ['1'], + originalValue: ['1'], }, ]; @@ -452,7 +455,6 @@ export default function ({ getService }: FtrProviderContext) { eventId: ID, }) .expect(200); - expect(sortBy(detailsData, 'name')).to.eql(sortBy(EXPECTED_DATA, 'name')); }); From 180f309fab1dec168a0a7ae81ac497f46966b15d Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Mon, 8 Feb 2021 16:05:26 -0500 Subject: [PATCH 05/19] Update security solution codeowners (#89038) * move the code coverage owner line for security solution next to the other lines about security solution * replace @elastic/siem with @elastic/security-solution and remove the duplicate code coverage owner for security_solution * remove elastic/endpoint-app-team and cleanup directories that no longer exist, and reorder lines * added case_api_integration code owners Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2917cc52a6c6d..b6c0c6afdee0b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -244,7 +244,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security -#CC# /x-pack/plugins/security_solution/ @elastic/kibana-security #CC# /x-pack/plugins/security/ @elastic/kibana-security # Kibana Alerting Services @@ -312,25 +311,22 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib #CC# /x-pack/plugins/console_extensions/ @elastic/es-ui #CC# /x-pack/plugins/cross_cluster_replication/ @elastic/es-ui -# Endpoint -/x-pack/plugins/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/security_solution_endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/functional/es_archives/endpoint/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/endpoint-app-team @elastic/siem -/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/endpoint-app-team @elastic/siem -#CC# /x-pack/legacy/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/siem/ @elastic/siem -#CC# /x-pack/plugins/security_solution/ @elastic/siem - # Security Solution -/x-pack/plugins/security_solution/ @elastic/siem @elastic/endpoint-app-team -/x-pack/test/detection_engine_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/lists_api_integration @elastic/siem @elastic/endpoint-app-team -/x-pack/test/api_integration/apis/security_solution @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/case @elastic/siem @elastic/endpoint-app-team -/x-pack/plugins/lists @elastic/siem @elastic/endpoint-app-team -#CC# /x-pack/plugins/security_solution/ @elastic/siem +/x-pack/test/endpoint_api_integration_no_ingest/ @elastic/security-solution +/x-pack/test/security_solution_endpoint/ @elastic/security-solution +/x-pack/test/functional/es_archives/endpoint/ @elastic/security-solution +/x-pack/test/plugin_functional/plugins/resolver_test/ @elastic/security-solution +/x-pack/test/plugin_functional/test_suites/resolver/ @elastic/security-solution +/x-pack/plugins/security_solution/ @elastic/security-solution +/x-pack/test/detection_engine_api_integration @elastic/security-solution +/x-pack/test/lists_api_integration @elastic/security-solution +/x-pack/test/api_integration/apis/security_solution @elastic/security-solution +#CC# /x-pack/plugins/security_solution/ @elastic/security-solution + +# Security Solution sub teams +/x-pack/plugins/case @elastic/security-threat-hunting +/x-pack/test/case_api_integration @elastic/security-threat-hunting +/x-pack/plugins/lists @elastic/security-detections-response # Security Intelligence And Analytics /x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules @elastic/security-intelligence-analytics @@ -362,3 +358,4 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Reporting #CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services + From 3fa76956ac3e963cd6d89f2f390b47e47d3f65b0 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 8 Feb 2021 16:08:23 -0500 Subject: [PATCH 06/19] [CI] Automated backports via GitHub Actions - initial MVP (#90669) --- .github/CODEOWNERS | 1 + .github/workflows/backport.yml | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .github/workflows/backport.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b6c0c6afdee0b..ec07a6a03d2c8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -149,6 +149,7 @@ /src/cli/keystore/ @elastic/kibana-operations /src/legacy/server/warnings/ @elastic/kibana-operations /.ci/es-snapshots/ @elastic/kibana-operations +/.github/workflows/ @elastic/kibana-operations /vars/ @elastic/kibana-operations /.bazelignore @elastic/kibana-operations /.bazeliskversion @elastic/kibana-operations diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000000000..f64b9e95fbaab --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,46 @@ +on: + pull_request_target: + branches: + - master + types: + - labeled + - closed + +jobs: + backport: + name: Backport PR + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'auto-backport') + runs-on: ubuntu-latest + + steps: + - name: 'Get backport config' + run: | + curl 'https://raw.githubusercontent.com/elastic/kibana/master/.backportrc.json' > .backportrc.json + + - name: Use Node.js 14.x + uses: actions/setup-node@v1 + with: + node-version: 14.x + + - name: Install backport CLI + run: npm install -g backport@5.6.4 + + - name: Backport PR + run: | + git config --global user.name "kibanamachine" + git config --global user.email "42973632+kibanamachine@users.noreply.github.com" + backport --fork true --username kibanamachine --accessToken "${{ secrets.KIBANAMACHINE_TOKEN }}" --ci --pr "$PR_NUMBER" --labels backport --assignee "$PR_OWNER" | tee 'output.log' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_OWNER: ${{ github.event.pull_request.user.login }} + + - name: Report backport status + run: | + COMMENT="Backport result + \`\`\` + $(cat output.log) + \`\`\`" + + GITHUB_TOKEN="${{ secrets.KIBANAMACHINE_TOKEN }}" gh api -X POST repos/elastic/kibana/issues/$PR_NUMBER/comments -F body="$COMMENT" + env: + PR_NUMBER: ${{ github.event.pull_request.number }} From 7a2b7550c962c5b6084f8f84e2dc9a2b5f3db0a2 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 8 Feb 2021 15:10:23 -0600 Subject: [PATCH 07/19] [DOCS] Fixes Dashboard formatting (#90485) * [DOCS] Fixes Dashboard formatting * Fixes the semi-structured search example * Update docs/user/dashboard/dashboard.asciidoc Co-authored-by: Wylie Conlon Co-authored-by: Wylie Conlon --- docs/user/dashboard/dashboard.asciidoc | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 8b3eddc008500..3c86c37f1fd30 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -133,15 +133,17 @@ image::dashboard/images/dashboard-filters.png[Labeled interface with semi-struct Semi-structured search:: Combine free text search with field-based search using the <>. Type a search term to match across all fields, or begin typing a field name to - get prompted with field names and operators you can use to build a structured query. - + + get prompted with field names and operators you can use to build a structured query. For example, in the sample web logs data, this query displays data only for the US: - . Enter `g`, and then select *geo.source*. - . Select *equals some value* and *US*, and then click *Update*. + . Enter `g`, then select *geo.source*. + . Select *equals some value* and *US*, then click *Update*. . For a more complex search, try: - `geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat"` +[source,text] +------------------- +geo.src : "US" and url.keyword : "https://www.elastic.co/downloads/beats/metricbeat" +------------------- Time filter:: Dashboards have a global time filter that restricts the data that displays, but individual panels can @@ -152,21 +154,18 @@ Time filter:: . Open the panel menu, then select *More > Customize time range*. . On the *Customize panel time range* window, specify the new time range, then click *Add to panel*. - + [role="screenshot"] image:images/time_range_per_panel.gif[Time range per dashboard panel] Additional filters with AND:: - You can add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. - + Add filters to a dashboard, or pin filters to multiple places in {kib}. To add filters, using a basic editor or an advanced JSON editor for the {es} {ref}/query-dsl.html[query DSL]. When you use more than one index pattern on a dashboard, the filter editor allows you to filter only one dashboard. - To dynamically add filters, click a series on a dashboard. For example, to filter the dashboard to display only ios data: - . Click *Add filter*. . Set *Field* to *machine.os*, *Operator* to *is*, and *Value* to *ios*. . *Save* the filter. - . To remove the filter, click *x* next to the filter. + . To remove the filter, click *x*. [float] [[clone-panels]] From 46feb7659279b98d07ae7c7a259cd666605ba6ae Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 8 Feb 2021 23:42:07 +0200 Subject: [PATCH 08/19] [Alerts] Jira: Disallow labels with spaces (#90548) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/actions/README.md | 2 +- .../builtin_action_types/jira/schema.ts | 10 ++++++- .../builtin_action_types/jira/jira.test.tsx | 19 ++++++++++++- .../builtin_action_types/jira/jira.tsx | 7 +++++ .../builtin_action_types/jira/jira_params.tsx | 8 ++++++ .../builtin_action_types/jira/translations.ts | 7 +++++ .../actions/builtin_action_types/jira.ts | 28 +++++++++++++++++++ 7 files changed, 78 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 1eb94af4dddf8..1d50bc7e05807 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -657,7 +657,7 @@ The following table describes the properties of the `incident` object. | externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | | issueType | The id of the issue type in Jira. | string _(optional)_ | | priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. | string[] _(optional)_ | +| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | | parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ | #### `subActionParams (getIncident)` diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 552053bdd7651..a81dfaeef8175 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -40,7 +40,15 @@ export const ExecutorSubActionPushParamsSchema = schema.object({ externalId: schema.nullable(schema.string()), issueType: schema.nullable(schema.string()), priority: schema.nullable(schema.string()), - labels: schema.nullable(schema.arrayOf(schema.string())), + labels: schema.nullable( + schema.arrayOf( + schema.string({ + validate: (label) => + // Matches any space, tab or newline character. + label.match(/\s/g) ? `The label ${label} cannot contain spaces` : undefined, + }) + ) + ), parent: schema.nullable(schema.string()), }), comments: schema.nullable( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx index 2d47740a581b8..ea1bcf82c314c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -96,7 +96,7 @@ describe('jira action params validation', () => { }; expect(actionTypeModel.validateParams(actionParams)).toEqual({ - errors: { 'subActionParams.incident.summary': [] }, + errors: { 'subActionParams.incident.summary': [], 'subActionParams.incident.labels': [] }, }); }); @@ -108,6 +108,23 @@ describe('jira action params validation', () => { expect(actionTypeModel.validateParams(actionParams)).toEqual({ errors: { 'subActionParams.incident.summary': ['Summary is required.'], + 'subActionParams.incident.labels': [], + }, + }); + }); + + test('params validation fails when labels contain spaces', () => { + const actionParams = { + subActionParams: { + incident: { summary: 'some title', labels: ['label with spaces'] }, + comments: [], + }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.summary': [], + 'subActionParams.incident.labels': ['Labels cannot contain spaces.'], }, }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 5cb8a76d09bee..26b37278003c3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -72,6 +72,7 @@ export function getActionType(): ActionTypeModel => { const errors = { 'subActionParams.incident.summary': new Array(), + 'subActionParams.incident.labels': new Array(), }; const validationResult = { errors, @@ -83,6 +84,12 @@ export function getActionType(): ActionTypeModel label.match(/\s/g))) + errors['subActionParams.incident.labels'].push(i18n.LABELS_WHITE_SPACES); + } return validationResult; }, actionParamsFields: lazy(() => import('./jira_params')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 75930482797a2..cb2d637972cb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -184,6 +184,11 @@ const JiraParamsFields: React.FunctionComponent 0 && + incident.labels !== undefined; + return ( <> @@ -304,6 +309,8 @@ const JiraParamsFields: React.FunctionComponent
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts index 3c8bda7792f0a..fe7ea61e68193 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -199,3 +199,10 @@ export const SEARCH_ISSUES_LOADING = i18n.translate( defaultMessage: 'Loading...', } ); + +export const LABELS_WHITE_SPACES = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.labelsSpacesErrorMessage', + { + defaultMessage: 'Labels cannot contain spaces.', + } +); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 6cc5e2eaefb94..8bd0b8a790d40 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -375,6 +375,34 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + it('should handle failing with a simulated success when labels containing a space', async () => { + await supertest + .post(`/api/actions/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockJira.params, + subActionParams: { + incident: { + ...mockJira.params.subActionParams.incident, + issueType: '10006', + labels: ['label with spaces'], + }, + comments: [], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.labels]: types that failed validation:\n - [subActionParams.incident.labels.0.0]: The label label with spaces cannot contain spaces\n - [subActionParams.incident.labels.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [issueTypes]\n- [5.subAction]: expected value to equal [fieldsByIssueType]\n- [6.subAction]: expected value to equal [issues]\n- [7.subAction]: expected value to equal [issue]', + }); + }); + }); }); describe('Execution', () => { From b39ad86b5d59fef96366affbdfbcc6758545cba9 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Mon, 8 Feb 2021 14:23:10 -0800 Subject: [PATCH 09/19] Use default ES distribution for functional tests (#88737) Signed-off-by: Tyler Smalley Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .ci/packer_cache_for_branch.sh | 1 - .../src/functional_test_runner/lib/config/schema.ts | 2 +- .../kbn-test/src/legacy_es/legacy_es_test_cluster.js | 2 +- .../migrationsv2/integration_tests/migration.test.ts | 2 +- .../migration_7.7.2_xpack_100k.test.ts | 2 +- src/core/test_helpers/kbn_server.ts | 4 ++-- src/dev/ci_setup/setup.sh | 2 -- test/api_integration/config.js | 5 ++++- test/common/config.js | 4 +--- test/common/services/deployment.ts | 12 +----------- test/examples/config.js | 5 ++++- test/functional/config.js | 6 ++++-- test/plugin_functional/config.ts | 5 ++++- test/server_integration/config.js | 5 ++++- test/server_integration/http/ssl/config.js | 5 ++++- test/server_integration/http/ssl_redirect/config.js | 5 ++++- test/server_integration/http/ssl_with_p12/config.js | 5 ++++- .../http/ssl_with_p12_intermediate/config.js | 5 ++++- 18 files changed, 44 insertions(+), 33 deletions(-) diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh index bbdf5484faf65..ee220537de340 100755 --- a/.ci/packer_cache_for_branch.sh +++ b/.ci/packer_cache_for_branch.sh @@ -26,7 +26,6 @@ source src/dev/ci_setup/setup.sh; # download es snapshots node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; # download reporting browsers (cd "x-pack" && node ../node_modules/.bin/gulp downloadChromium); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 46471a4e9dac7..4fd28678d2653 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -169,7 +169,7 @@ export const schema = Joi.object() esTestCluster: Joi.object() .keys({ - license: Joi.string().default('oss'), + license: Joi.string().default('basic'), from: Joi.string().default('snapshot'), serverArgs: Joi.array(), serverEnvVars: Joi.object(), diff --git a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js index c04564279a971..43b6c90452b81 100644 --- a/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js +++ b/packages/kbn-test/src/legacy_es/legacy_es_test_cluster.js @@ -22,7 +22,7 @@ export function createLegacyEsTestCluster(options = {}) { const { port = esTestConfig.getPort(), password = 'changeme', - license = 'oss', + license = 'basic', log, basePath = resolve(KIBANA_ROOT, '.es'), esFrom = esTestConfig.getBuildFrom(), diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index 2d3ab91697e42..317bfe33b3a19 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -30,7 +30,7 @@ describe('migration v2', () => { adjustTimeout: (t: number) => jest.setTimeout(t), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts index bce01c93fe886..16ba0c855867c 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts @@ -32,7 +32,7 @@ describe.skip('migration from 7.7.2-xpack with 100k objects', () => { adjustTimeout: (t: number) => jest.setTimeout(600000), settings: { es: { - license: oss ? 'oss' : 'trial', + license: 'trial', dataArchive, }, }, diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index 011ba67a05512..14f614643ac9f 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -185,7 +185,7 @@ export function createTestServers({ adjustTimeout: (timeout: number) => void; settings?: { es?: { - license: 'oss' | 'basic' | 'gold' | 'trial'; + license: 'basic' | 'gold' | 'trial'; [key: string]: any; }; kbn?: { @@ -208,7 +208,7 @@ export function createTestServers({ if (!adjustTimeout) { throw new Error('adjustTimeout is required in order to avoid flaky tests'); } - const license = get(settings, 'es.license', 'oss'); + const license = get(settings, 'es.license', 'basic'); const usersToBeAdded = get(settings, 'users', []); if (usersToBeAdded.length > 0) { if (license !== 'trial') { diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index 0b24f0b22b81a..db7110d2d0875 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -32,8 +32,6 @@ yarn kbn bootstrap ### echo " -- downloading es snapshot" node scripts/es snapshot --download-only; -node scripts/es snapshot --license=oss --download-only; - ### ### verify no git modifications diff --git a/test/api_integration/config.js b/test/api_integration/config.js index d688c31dc47e7..bd8f10606a45a 100644 --- a/test/api_integration/config.js +++ b/test/api_integration/config.js @@ -19,7 +19,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'API Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/common/config.js b/test/common/config.js index 9d108f05fd1fc..46cd07b2ec370 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -21,9 +21,7 @@ export default function () { servers, esTestCluster: { - license: 'oss', - from: 'snapshot', - serverArgs: [], + serverArgs: ['xpack.security.enabled=false'], }, kbnTestServer: { diff --git a/test/common/services/deployment.ts b/test/common/services/deployment.ts index a19118bb3065a..510124ce3d1b7 100644 --- a/test/common/services/deployment.ts +++ b/test/common/services/deployment.ts @@ -35,17 +35,7 @@ export function DeploymentProvider({ getService }: FtrProviderContext) { * Useful for functional testing in cloud environment */ async isOss() { - const baseUrl = this.getEsHostPort(); - const username = config.get('servers.elasticsearch.username'); - const password = config.get('servers.elasticsearch.password'); - const response = await fetch(baseUrl + '/_xpack', { - method: 'get', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Basic ' + Buffer.from(username + ':' + password).toString('base64'), - }, - }); - return response.status !== 200; + return config.get('kbnTestServer.serverArgs').indexOf('--oss') > -1; }, async isCloud(): Promise { diff --git a/test/examples/config.js b/test/examples/config.js index fd1ad671cf4bf..0ba7af0bfceb7 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -34,7 +34,10 @@ export default async function ({ readConfigFile }) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/functional/config.js b/test/functional/config.js index c15cfffbdb576..05d6cf9dd6b68 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -32,8 +32,10 @@ export default async function ({ readConfigFile }) { servers: commonConfig.get('servers'), - esTestCluster: commonConfig.get('esTestCluster'), - + esTestCluster: { + ...commonConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...commonConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/plugin_functional/config.ts b/test/plugin_functional/config.ts index f28e219884bde..bd5ef814ae6c0 100644 --- a/test/plugin_functional/config.ts +++ b/test/plugin_functional/config.ts @@ -36,7 +36,10 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { }, pageObjects: functionalConfig.get('pageObjects'), servers: functionalConfig.get('servers'), - esTestCluster: functionalConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, apps: functionalConfig.get('apps'), esArchiver: { directory: path.resolve(__dirname, '../es_archives'), diff --git a/test/server_integration/config.js b/test/server_integration/config.js index 7171a9b33bfd8..0ebb5c48033b8 100644 --- a/test/server_integration/config.js +++ b/test/server_integration/config.js @@ -27,7 +27,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Integration Tests', }, - esTestCluster: commonConfig.get('esTestCluster'), + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...functionalConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index b305728b64de2..14381de6667fd 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index 0c3e8ce78237a..d19883bcfe241 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -44,7 +44,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js index 75a33226aa669..c4621500e927d 100644 --- a/test/server_integration/http/ssl_with_p12/config.js +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js index a120ea0b3a556..7f32bad648351 100644 --- a/test/server_integration/http/ssl_with_p12_intermediate/config.js +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -33,7 +33,10 @@ export default async function ({ readConfigFile }) { junit: { reportName: 'Http SSL Integration Tests', }, - esTestCluster: httpConfig.get('esTestCluster'), + esTestCluster: { + ...httpConfig.get('esTestCluster'), + serverArgs: ['xpack.security.enabled=false'], + }, kbnTestServer: { ...httpConfig.get('kbnTestServer'), serverArgs: [ From 15a4c285b860904b6546519ba3905b85a260554a Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 Feb 2021 22:24:14 +0000 Subject: [PATCH 10/19] chore(NA): move bazel workspace status from bash script into nodejs executable (#90560) * chore(NA): move bazel workspace status into nodejs executable * chore(NA): removed unused console.log on error * chore(NA): ability to setup different name for origin remote on workspace status command * chore(NA): do not fail if cant collect repo url Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .bazelrc | 2 +- src/dev/bazel_workspace_status.js | 80 +++++++++++++++++++++++++++++++ src/dev/bazel_workspace_status.sh | 57 ---------------------- 3 files changed, 81 insertions(+), 58 deletions(-) create mode 100644 src/dev/bazel_workspace_status.js delete mode 100755 src/dev/bazel_workspace_status.sh diff --git a/.bazelrc b/.bazelrc index 158338ec5f093..5fa6ef245fcea 100644 --- a/.bazelrc +++ b/.bazelrc @@ -11,7 +11,7 @@ import %workspace%/.bazelrc.common # BuildBuddy ## Metadata settings -build --workspace_status_command=$(pwd)/src/dev/bazel_workspace_status.sh +build --workspace_status_command="node ./src/dev/bazel_workspace_status.js" # Enable this in case you want to share your build info # build --build_metadata=VISIBILITY=PUBLIC build --build_metadata=TEST_GROUPS=//packages diff --git a/src/dev/bazel_workspace_status.js b/src/dev/bazel_workspace_status.js new file mode 100644 index 0000000000000..fe60f9176d243 --- /dev/null +++ b/src/dev/bazel_workspace_status.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +// Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh +// This script will be run bazel when building process starts to +// generate key-value information that represents the status of the +// workspace. The output should be like +// +// KEY1 VALUE1 +// KEY2 VALUE2 +// +// If the script exits with non-zero code, it's considered as a failure +// and the output will be discarded. + +(async () => { + const execa = require('execa'); + const os = require('os'); + + async function runCmd(cmd, args) { + try { + return await execa(cmd, args); + } catch (e) { + return { exitCode: 1 }; + } + } + + // Git repo + const kbnGitOriginName = process.env.KBN_GIT_ORIGIN_NAME || 'origin'; + const repoUrlCmdResult = await runCmd('git', [ + 'config', + '--get', + `remote.${kbnGitOriginName}.url`, + ]); + if (repoUrlCmdResult.exitCode === 0) { + // Only output REPO_URL when found it + console.log(`REPO_URL ${repoUrlCmdResult.stdout}`); + } + + // Commit SHA + const commitSHACmdResult = await runCmd('git', ['rev-parse', 'HEAD']); + if (commitSHACmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`COMMIT_SHA ${commitSHACmdResult.stdout}`); + + // Git branch + const gitBranchCmdResult = await runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD']); + if (gitBranchCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`GIT_BRANCH ${gitBranchCmdResult.stdout}`); + + // Tree status + const treeStatusCmdResult = await runCmd('git', ['diff-index', '--quiet', 'HEAD', '--']); + const treeStatusVarStr = 'GIT_TREE_STATUS'; + if (treeStatusCmdResult.exitCode === 0) { + console.log(`${treeStatusVarStr} Clean`); + } else { + console.log(`${treeStatusVarStr} Modified`); + } + + // Host + if (process.env.CI) { + const hostCmdResult = await runCmd('hostname'); + const hostStr = hostCmdResult.stdout.split('-').slice(0, -1).join('-'); + const coresStr = os.cpus().filter((cpu, index) => { + return !cpu.model.includes('Intel') || index % 2 === 1; + }).length; + + if (hostCmdResult.exitCode !== 0) { + process.exit(1); + } + console.log(`HOST ${hostStr}-${coresStr}`); + } +})(); diff --git a/src/dev/bazel_workspace_status.sh b/src/dev/bazel_workspace_status.sh deleted file mode 100755 index efaca4bb98849..0000000000000 --- a/src/dev/bazel_workspace_status.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# Inspired on https://github.com/buildbuddy-io/buildbuddy/blob/master/workspace_status.sh -# This script will be run bazel when building process starts to -# generate key-value information that represents the status of the -# workspace. The output should be like -# -# KEY1 VALUE1 -# KEY2 VALUE2 -# -# If the script exits with non-zero code, it's considered as a failure -# and the output will be discarded. - -# Git repo -repo_url=$(git config --get remote.origin.url) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "REPO_URL ${repo_url}" - -# Commit SHA -commit_sha=$(git rev-parse HEAD) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "COMMIT_SHA ${commit_sha}" - -# Git branch -repo_url=$(git rev-parse --abbrev-ref HEAD) -if [[ $? != 0 ]]; -then - exit 1 -fi -echo "GIT_BRANCH ${repo_url}" - -# Tree status -git diff-index --quiet HEAD -- -if [[ $? == 0 ]]; -then - tree_status="Clean" -else - tree_status="Modified" -fi -echo "GIT_TREE_STATUS ${tree_status}" - -# Host -if [ "$CI" = "true" ]; then - host=$(hostname | sed 's|\(.*\)-.*|\1|') - cores=$(grep ^cpu\\scores /proc/cpuinfo | uniq | awk '{print $4}' ) - if [[ $? != 0 ]]; - then - exit 1 - fi - echo "HOST ${host}-${cores}" -fi From 54863889d496361239dea50360b68f390160e03c Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 8 Feb 2021 22:25:05 +0000 Subject: [PATCH 11/19] chore(NA): remove write permissions on Bazel remote cache for PRs (#90652) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/ci_setup/setup_env.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 0b835d4b9fa94..2deafaaf35a94 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -180,6 +180,15 @@ fi ### cp -f "$KIBANA_DIR/src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; +### +### remove write permissions on buildbuddy remote cache for prs +### +if [[ "$ghprbPullId" ]] ; then + echo "# Appended by $KIBANA_DIR/src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" + echo "# Uploads logs & artifacts without writing to cache" >> "$HOME/.bazelrc" + echo "build --noremote_upload_local_results" >> "$HOME/.bazelrc" +fi + ### ### append auth token to buildbuddy into "$HOME/.bazelrc"; ### From 3ef3e9324fd9d77dd442b09e3355fb005c4b3caf Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 8 Feb 2021 14:46:57 -0800 Subject: [PATCH 12/19] [dev-utils/ship-ci-stats] fail when CI stats is down (#90678) Co-authored-by: spalger --- .../src/ci_stats_reporter/ci_stats_reporter.ts | 2 +- .../src/ci_stats_reporter/ship_ci_stats_cli.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index cb5175142c160..93826cf3add80 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -109,7 +109,7 @@ export class CiStatsReporter { }, }); - return; + return true; } catch (error) { if (!error?.request) { // not an axios error, must be a usage error that we should notify user about diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts index 244af7b657418..1ee78518bb801 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ship_ci_stats_cli.ts @@ -10,7 +10,7 @@ import Path from 'path'; import Fs from 'fs'; import { CiStatsReporter } from './ci_stats_reporter'; -import { run, createFlagError } from '../run'; +import { run, createFlagError, createFailError } from '../run'; export function shipCiStatsCli() { run( @@ -23,12 +23,20 @@ export function shipCiStatsCli() { } const reporter = CiStatsReporter.fromEnv(log); + + if (!reporter.isEnabled()) { + throw createFailError('unable to initilize the CI Stats reporter'); + } + for (const path of metricPaths) { // resolve path from CLI relative to CWD const abs = Path.resolve(path); const json = Fs.readFileSync(abs, 'utf8'); - await reporter.metrics(JSON.parse(json)); - log.success('shipped metrics from', path); + if (await reporter.metrics(JSON.parse(json))) { + log.success('shipped metrics from', path); + } else { + throw createFailError('failed to ship metrics'); + } } }, { From 3cf00d2bb40c43471327501752ce099ce7f36a21 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Mon, 8 Feb 2021 15:21:06 -0800 Subject: [PATCH 13/19] [Time to Visualize] Adds functional tests for linking/unlinking panel from embeddable library (#89612) --- .github/CODEOWNERS | 1 + .../actions/add_to_library_action.tsx | 2 +- .../apps/dashboard/embeddable_library.ts | 111 ++++++++++++++++++ test/functional/apps/dashboard/index.ts | 1 + .../services/dashboard/panel_actions.ts | 25 ++++ x-pack/test/functional/apps/lens/dashboard.ts | 38 ++++++ .../maps/embeddable/embeddable_library.js | 80 +++++++++++++ .../functional/apps/maps/embeddable/index.js | 1 + 8 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 test/functional/apps/dashboard/embeddable_library.ts create mode 100644 x-pack/test/functional/apps/maps/embeddable/embeddable_library.js diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ec07a6a03d2c8..34b449346ddf7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -91,6 +91,7 @@ /src/plugins/dashboard/ @elastic/kibana-presentation /src/plugins/input_control_vis/ @elastic/kibana-presentation /src/plugins/vis_type_markdown/ @elastic/kibana-presentation +/test/functional/apps/dashboard/ @elastic/kibana-presentation /x-pack/plugins/canvas/ @elastic/kibana-presentation /x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx index 5d384ed8ebd82..ef730e16bc5cf 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.tsx @@ -22,7 +22,7 @@ import { NotificationsStart } from '../../services/core'; import { dashboardAddToLibraryAction } from '../../dashboard_strings'; import { DashboardPanelState, DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '..'; -export const ACTION_ADD_TO_LIBRARY = 'addToFromLibrary'; +export const ACTION_ADD_TO_LIBRARY = 'saveToLibrary'; export interface AddToLibraryActionContext { embeddable: IEmbeddable; diff --git a/test/functional/apps/dashboard/embeddable_library.ts b/test/functional/apps/dashboard/embeddable_library.ts new file mode 100644 index 0000000000000..20fe9aeb1387a --- /dev/null +++ b/test/functional/apps/dashboard/embeddable_library.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'settings', 'common']); + const esArchiver = getService('esArchiver'); + const find = getService('find'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const panelActions = getService('dashboardPanelActions'); + + describe('embeddable library', () => { + before(async () => { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('unlink visualize panel from embeddable library', async () => { + // add heatmap panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.clickByButtonText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: heatmap'); + await find.existsByLinkText('Rendering Test: heatmap'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save visualize panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:heatmap'); + await panelActions.saveToLibrary('Rendering Test: heatmap - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:heatmap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + // add map panel from library + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.clickByButtonText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('Rendering Test: geo map'); + await find.existsByLinkText('Rendering Test: geo map'); + await dashboardAddPanel.closeAddPanel(); + }); + + it('save map panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-RenderingTest:geomap'); + await panelActions.saveToLibrary('Rendering Test: geo map - copy', originalPanel); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find( + 'embeddablePanelHeading-RenderingTest:geomap-copy' + ); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index 9332503539874..b71a89501fbf6 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -81,6 +81,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. // If we don't use the timestamp in the URL, the colors in the charts will be different. loadTestFile(require.resolve('./dashboard_snapshots')); + loadTestFile(require.resolve('./embeddable_library')); }); // Each of these tests call initTests themselves, the way it was originally written. The above tests only load diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 534d4cebd92f4..881e3ad4157a4 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -17,6 +17,8 @@ const TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-togglePanel'; const CUSTOMIZE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'; const OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ = 'embeddablePanelToggleMenuIcon'; const OPEN_INSPECTOR_TEST_SUBJ = 'embeddablePanelAction-openInspector'; +const LIBRARY_NOTIFICATION_TEST_SUBJ = 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'; +const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary'; export function DashboardPanelActionsProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); @@ -170,6 +172,29 @@ export function DashboardPanelActionsProvider({ getService, getPageObjects }: Ft await testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ); } + async unlinkFromLibary(parent?: WebElementWrapper) { + log.debug('unlinkFromLibrary'); + const libraryNotification = parent + ? await testSubjects.findDescendant(LIBRARY_NOTIFICATION_TEST_SUBJ, parent) + : await testSubjects.find(LIBRARY_NOTIFICATION_TEST_SUBJ); + await libraryNotification.click(); + await testSubjects.click('libraryNotificationUnlinkButton'); + } + + async saveToLibrary(newTitle: string, parent?: WebElementWrapper) { + log.debug('saveToLibrary'); + await this.openContextMenu(parent); + const exists = await testSubjects.exists(SAVE_TO_LIBRARY_TEST_SUBJ); + if (!exists) { + await this.clickContextMenuMoreItem(); + } + await testSubjects.click(SAVE_TO_LIBRARY_TEST_SUBJ); + await testSubjects.setValue('savedObjectTitle', newTitle, { + clearWithKeyboard: true, + }); + await testSubjects.click('confirmSaveSavedObjectButton'); + } + async expectExistsRemovePanelAction() { log.debug('expectExistsRemovePanelAction'); await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); diff --git a/x-pack/test/functional/apps/lens/dashboard.ts b/x-pack/test/functional/apps/lens/dashboard.ts index 738e45c1cbcf1..5cbd5dff45e1e 100644 --- a/x-pack/test/functional/apps/lens/dashboard.ts +++ b/x-pack/test/functional/apps/lens/dashboard.ts @@ -156,5 +156,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await panelActions.clickContextMenuMoreItem(); await testSubjects.existOrFail(ACTION_TEST_SUBJ); }); + + it('unlink lens panel from embeddable library', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.clickByButtonText('lnsPieVis'); + await dashboardAddPanel.closeAddPanel(); + + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + }); + + it('save lens panel to embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis'); + await panelActions.saveToLibrary('lnsPieVis - copy', originalPanel); + await testSubjects.click('confirmSaveSavedObjectButton'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-lnsPieVis-copy'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(true); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('lnsPieVis'); + await find.existsByLinkText('lnsPieVis'); + }); }); } diff --git a/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js new file mode 100644 index 0000000000000..40e73f0d8a763 --- /dev/null +++ b/x-pack/test/functional/apps/maps/embeddable/embeddable_library.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +export default function ({ getPageObjects, getService }) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'maps', 'visualize']); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardVisualizations = getService('dashboardVisualizations'); + + describe('maps in embeddable library', () => { + before(async () => { + await security.testUser.setRoles( + [ + 'test_logstash_reader', + 'global_maps_all', + 'geoshape_data_reader', + 'global_dashboard_all', + 'meta_for_geoshape_data_reader', + ], + false + ); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'c698b940-e149-11e8-a35a-370a8516603a', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await dashboardAddPanel.clickCreateNewLink(); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMapsApp(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.maps.waitForLayersToLoad(); + await PageObjects.maps.clickSaveAndReturnButton(); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('save map panel to embeddable library', async () => { + await dashboardPanelActions.saveToLibrary('embeddable library map'); + await testSubjects.existOrFail('addPanelToLibrarySuccess'); + + const mapPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + mapPanel + ); + expect(libraryActionExists).to.be(true); + }); + + it('unlink map panel from embeddable library', async () => { + const originalPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + await dashboardPanelActions.unlinkFromLibary(originalPanel); + await testSubjects.existOrFail('unlinkPanelSuccess'); + + const updatedPanel = await testSubjects.find('embeddablePanelHeading-embeddablelibrarymap'); + const libraryActionExists = await testSubjects.descendantExists( + 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION', + updatedPanel + ); + expect(libraryActionExists).to.be(false); + + await dashboardAddPanel.clickOpenAddPanel(); + await dashboardAddPanel.filterEmbeddableNames('embeddable library map'); + await find.existsByLinkText('embeddable library map'); + await dashboardAddPanel.closeAddPanel(); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/embeddable/index.js index 815de2e081309..9fd4c9db703db 100644 --- a/x-pack/test/functional/apps/maps/embeddable/index.js +++ b/x-pack/test/functional/apps/maps/embeddable/index.js @@ -9,6 +9,7 @@ export default function ({ loadTestFile }) { describe('embeddable', function () { loadTestFile(require.resolve('./save_and_return')); loadTestFile(require.resolve('./dashboard')); + loadTestFile(require.resolve('./embeddable_library')); loadTestFile(require.resolve('./embeddable_state')); loadTestFile(require.resolve('./tooltip_filter_actions')); }); From 87212e68f71dab0b827cecc78d8e60c9eb821298 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Mon, 8 Feb 2021 19:08:15 -0500 Subject: [PATCH 14/19] [Upgrade Assistant] Add A11y Tests (#90265) * Added overview a11y test. Added data test subjects to the tabs in the app. * Added data test subjects for all tabs and the detail panel on the indidual pages. Updated tests to wait for detail panel to be visible before taking snapshot. * Updated snapshot for upgrade assistant jest tests. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/components/tabs.tsx | 3 ++ .../__snapshots__/checkup_tab.test.tsx.snap | 12 +++-- .../components/tabs/checkup/checkup_tab.tsx | 6 ++- .../components/tabs/overview/index.tsx | 2 +- .../accessibility/apps/upgrade_assistant.ts | 44 +++++++++++++++++++ x-pack/test/accessibility/config.ts | 1 + 6 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/accessibility/apps/upgrade_assistant.ts diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index d77349e53b354..fa6badb34635b 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -190,6 +190,7 @@ export class UpgradeAssistantTabs extends React.Component { return [ { id: 'overview', + 'data-test-subj': 'upgradeAssistantOverviewTab', name: i18n.translate('xpack.upgradeAssistant.overviewTab.overviewTabTitle', { defaultMessage: 'Overview', }), @@ -197,6 +198,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'cluster', + 'data-test-subj': 'upgradeAssistantClusterTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.clusterTabLabel', { defaultMessage: 'Cluster', }), @@ -213,6 +215,7 @@ export class UpgradeAssistantTabs extends React.Component { }, { id: 'indices', + 'data-test-subj': 'upgradeAssistantIndicesTab', name: i18n.translate('xpack.upgradeAssistant.checkupTab.indicesTabLabel', { defaultMessage: 'Indices', }), diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap index 5aa4a469e4f02..bac67bf722ea7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/__snapshots__/checkup_tab.test.tsx.snap @@ -6,7 +6,9 @@ exports[`CheckupTab render with deprecations 1`] = ` -

+

-

+

-

+

= ({ <> -

+

= (props) <> - +

{ + before(async () => { + await PageObjects.upgradeAssistant.navigateToPage(); + }); + + it('Overview Tab', async () => { + await retry.waitFor('Upgrade Assistant overview tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantOverviewTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Cluster Tab', async () => { + await testSubjects.click('upgradeAssistantClusterTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantClusterTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + + it('Indices Tab', async () => { + await testSubjects.click('upgradeAssistantIndicesTab'); + await retry.waitFor('Upgrade Assistant Cluster tab to be visible', async () => { + return testSubjects.exists('upgradeAssistantIndexTabDetail'); + }); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 94a09e3f767f6..24c46c1a1687e 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -31,6 +31,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/index_lifecycle_management'), require.resolve('./apps/ml'), require.resolve('./apps/lens'), + require.resolve('./apps/upgrade_assistant'), ], pageObjects, From 231610c7204ac5df84083cb39ab5d4c860a2ef5c Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 8 Feb 2021 21:50:07 -0500 Subject: [PATCH 15/19] [Monitoring] Migrate data source for legacy alerts to monitoring data directly (#87377) * License expiration * Fetch legacy alert data from the source * Add back in the one test file * Remove deprecated code * Fix up tests * Add test files * Fix i18n * Update tests * PR feedback * Fix types and tests * Fix license headers * Remove unused function * Fix faulty license expiration logic --- .../plugins/monitoring/common/types/alerts.ts | 51 ++++- x-pack/plugins/monitoring/common/types/es.ts | 6 +- .../monitoring/server/alerts/base_alert.ts | 88 +------- .../alerts/cluster_health_alert.test.ts | 50 +++-- .../server/alerts/cluster_health_alert.ts | 147 ++++++++----- ...asticsearch_version_mismatch_alert.test.ts | 58 +++-- .../elasticsearch_version_mismatch_alert.ts | 152 ++++++++----- .../kibana_version_mismatch_alert.test.ts | 57 +++-- .../alerts/kibana_version_mismatch_alert.ts | 157 ++++++++----- .../alerts/license_expiration_alert.test.ts | 102 ++++++--- .../server/alerts/license_expiration_alert.ts | 159 +++++++++----- .../logstash_version_mismatch_alert.test.ts | 57 +++-- .../alerts/logstash_version_mismatch_alert.ts | 152 ++++++++----- .../server/alerts/nodes_changed_alert.test.ts | 97 +++++++-- .../server/alerts/nodes_changed_alert.ts | 206 ++++++++++++------ .../lib/alerts/fetch_cluster_health.test.ts | 42 ++++ .../server/lib/alerts/fetch_cluster_health.ts | 69 ++++++ .../fetch_elasticsearch_versions.test.ts | 52 +++++ .../alerts/fetch_elasticsearch_versions.ts | 71 ++++++ .../lib/alerts/fetch_kibana_versions.test.ts | 74 +++++++ .../lib/alerts/fetch_kibana_versions.ts | 111 ++++++++++ .../lib/alerts/fetch_legacy_alerts.test.ts | 96 -------- .../server/lib/alerts/fetch_legacy_alerts.ts | 97 --------- .../server/lib/alerts/fetch_licenses.test.ts | 61 ++++++ .../server/lib/alerts/fetch_licenses.ts | 75 +++++++ .../alerts/fetch_logstash_versions.test.ts | 74 +++++++ .../lib/alerts/fetch_logstash_versions.ts | 111 ++++++++++ .../alerts/fetch_nodes_from_cluster_stats.ts | 105 +++++++++ .../server/lib/alerts/fetch_status.ts | 2 +- .../server/routes/api/v1/alerts/enable.ts | 3 +- .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - 32 files changed, 1802 insertions(+), 792 deletions(-) create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts create mode 100644 x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 7fb41ece527a1..649b92cb7ac82 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -6,7 +6,12 @@ */ import { Alert, AlertTypeParams, SanitizedAlert } from '../../../alerts/common'; -import { AlertParamType, AlertMessageTokenType, AlertSeverity } from '../enums'; +import { + AlertParamType, + AlertMessageTokenType, + AlertSeverity, + AlertClusterHealthType, +} from '../enums'; export type CommonAlert = Alert | SanitizedAlert; @@ -60,6 +65,8 @@ export interface AlertInstanceState { | AlertDiskUsageState | AlertThreadPoolRejectionsState | AlertNodeState + | AlertLicenseState + | AlertNodesChangedState >; [x: string]: unknown; } @@ -74,6 +81,7 @@ export interface AlertState { export interface AlertNodeState extends AlertState { nodeId: string; nodeName?: string; + meta: any; [key: string]: unknown; } @@ -96,6 +104,14 @@ export interface AlertThreadPoolRejectionsState extends AlertState { nodeName?: string; } +export interface AlertLicenseState extends AlertState { + expiryDateMS: number; +} + +export interface AlertNodesChangedState extends AlertState { + node: AlertClusterStatsNode; +} + export interface AlertUiState { isFiring: boolean; resolvedMS?: number; @@ -228,3 +244,36 @@ export interface LegacyAlertNodesChangedList { added: { [nodeName: string]: string }; restarted: { [nodeName: string]: string }; } + +export interface AlertLicense { + status: string; + type: string; + expiryDateMS: number; + clusterUuid: string; + ccs?: string; +} + +export interface AlertClusterStatsNodes { + clusterUuid: string; + recentNodes: AlertClusterStatsNode[]; + priorNodes: AlertClusterStatsNode[]; + ccs?: string; +} + +export interface AlertClusterStatsNode { + nodeUuid: string; + nodeEphemeralId?: string; + nodeName?: string; +} + +export interface AlertClusterHealth { + health: AlertClusterHealthType; + clusterUuid: string; + ccs?: string; +} + +export interface AlertVersions { + clusterUuid: string; + ccs?: string; + versions: string[]; +} diff --git a/x-pack/plugins/monitoring/common/types/es.ts b/x-pack/plugins/monitoring/common/types/es.ts index cb3d44d0080ed..9dce32211f4b1 100644 --- a/x-pack/plugins/monitoring/common/types/es.ts +++ b/x-pack/plugins/monitoring/common/types/es.ts @@ -154,7 +154,10 @@ export interface ElasticsearchLegacySource { cluster_state?: { status?: string; nodes?: { - [nodeUuid: string]: {}; + [nodeUuid: string]: { + ephemeral_id?: string; + name?: string; + }; }; master_node?: boolean; }; @@ -170,6 +173,7 @@ export interface ElasticsearchLegacySource { license?: { status?: string; type?: string; + expiry_date_in_millis?: number; }; logstash_state?: { pipeline?: { diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 200c61b29b2e0..e79eb78f7f66b 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -26,26 +26,16 @@ import { AlertEnableAction, CommonAlertFilter, CommonAlertParams, - LegacyAlert, } from '../../common/types/alerts'; import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../common/constants'; +import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; -import { MonitoringLicenseService } from '../types'; import { mbSafeQuery } from '../lib/mb_safe_query'; import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; import { parseDuration } from '../../../alerts/common/parse_duration'; import { Globals } from '../static_globals'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; -import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; - -interface LegacyOptions { - watchName: string; - nodeNameLabel: string; - changeDataValues?: Partial; -} type ExecutedState = | { @@ -60,7 +50,6 @@ interface AlertOptions { name: string; throttle?: string | null; interval?: string; - legacy?: LegacyOptions; defaultParams?: Partial; actionVariables: Array<{ name: string; description: string }>; fetchClustersRange?: number; @@ -126,16 +115,6 @@ export class BaseAlert { }; } - public isEnabled(licenseService: MonitoringLicenseService) { - if (this.alertOptions.legacy) { - const watcherFeature = licenseService.getWatcherFeature(); - if (!watcherFeature.isAvailable || !watcherFeature.isEnabled) { - return false; - } - } - return true; - } - public getId() { return this.rawAlert?.id; } @@ -271,10 +250,6 @@ export class BaseAlert { params as CommonAlertParams, availableCcs ); - if (this.alertOptions.legacy) { - const data = await this.fetchLegacyData(callCluster, clusters, availableCcs); - return await this.processLegacyData(data, clusters, services, state); - } const data = await this.fetchData(params, callCluster, clusters, availableCcs); return await this.processData(data, clusters, services, state); } @@ -312,35 +287,6 @@ export class BaseAlert { throw new Error('Child classes must implement `fetchData`'); } - protected async fetchLegacyData( - callCluster: CallCluster, - clusters: AlertCluster[], - availableCcs: string[] - ): Promise { - let alertIndexPattern = INDEX_ALERTS; - if (availableCcs) { - alertIndexPattern = getCcsIndexPattern(alertIndexPattern, availableCcs); - } - const legacyAlerts = await fetchLegacyAlerts( - callCluster, - clusters, - alertIndexPattern, - this.alertOptions.legacy!.watchName, - Globals.app.config.ui.max_bucket_size - ); - - return legacyAlerts.map((legacyAlert) => { - return { - clusterUuid: legacyAlert.metadata.cluster_uuid, - shouldFire: !legacyAlert.resolved_timestamp, - severity: mapLegacySeverity(legacyAlert.metadata.severity), - meta: legacyAlert, - nodeName: this.alertOptions.legacy!.nodeNameLabel, - ...this.alertOptions.legacy!.changeDataValues, - }; - }); - } - protected async processData( data: AlertData[], clusters: AlertCluster[], @@ -395,34 +341,6 @@ export class BaseAlert { return state; } - protected async processLegacyData( - data: AlertData[], - clusters: AlertCluster[], - services: AlertServices, - state: ExecutedState - ) { - const currentUTC = +new Date(); - for (const item of data) { - const instanceId = `${this.alertOptions.id}:${item.clusterUuid}`; - const instance = services.alertInstanceFactory(instanceId); - if (!item.shouldFire) { - instance.replaceState({ alertStates: [] }); - continue; - } - const cluster = clusters.find((c: AlertCluster) => c.clusterUuid === item.clusterUuid); - const alertState: AlertState = this.getDefaultAlertState(cluster!, item); - alertState.nodeName = item.nodeName; - alertState.ui.triggeredMS = currentUTC; - alertState.ui.isFiring = true; - alertState.ui.severity = item.severity; - alertState.ui.message = this.getUiMessage(alertState, item); - instance.replaceState({ alertStates: [alertState] }); - this.executeActions(instance, alertState, item, cluster); - } - state.lastChecked = currentUTC; - return state; - } - protected getDefaultAlertState(cluster: AlertCluster, item: AlertData): AlertState { return { cluster, @@ -437,10 +355,6 @@ export class BaseAlert { }; } - protected getVersions(legacyAlert: LegacyAlert) { - return `[${legacyAlert.message.match(/(?<=Versions: \[).+?(?=\])/)}]`; - } - protected getUiMessage( alertState: AlertState | unknown, item: AlertData | unknown diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index 3d8000d317526..1490a6ce58e04 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -7,7 +7,8 @@ import { ClusterHealthAlert } from './cluster_health_alert'; import { ALERT_CLUSTER_HEALTH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { AlertClusterHealthType, AlertSeverity } from '../../common/enums'; +import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; @@ -26,8 +27,8 @@ jest.mock('../static_globals', () => ({ }, })); -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_cluster_health', () => ({ + fetchClusterHealth: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -63,16 +64,16 @@ describe('ClusterHealthAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'Elasticsearch cluster status is yellow.', - message: 'Allocate missing replica shards.', - metadata: { - severity: 2000, - cluster_uuid: clusterUuid, + const healths = [ + { + health: AlertClusterHealthType.Yellow, + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -94,8 +95,8 @@ describe('ClusterHealthAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchClusterHealth as jest.Mock).mockImplementation(() => { + return healths; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -120,8 +121,15 @@ describe('ClusterHealthAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Elasticsearch cluster alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + health: AlertClusterHealthType.Yellow, + }, ui: { isFiring: true, message: { @@ -140,7 +148,7 @@ describe('ClusterHealthAlert', () => { }, ], }, - severity: 'danger', + severity: AlertSeverity.Warning, triggeredMS: 1, lastCheckedMS: 0, }, @@ -160,9 +168,15 @@ describe('ClusterHealthAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if the cluster health is green', async () => { + (fetchClusterHealth as jest.Mock).mockImplementation(() => { + return [ + { + health: AlertClusterHealthType.Green, + clusterUuid, + ccs, + }, + ]; }); const alert = new ClusterHealthAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index 63f658d5b0283..c4e5de3d55356 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -13,13 +13,23 @@ import { AlertState, AlertMessage, AlertMessageLinkToken, - LegacyAlert, + CommonAlertParams, + AlertClusterHealth, + AlertInstanceState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_CLUSTER_HEALTH, LEGACY_ALERT_DETAILS } from '../../common/constants'; -import { AlertMessageTokenType, AlertClusterHealthType } from '../../common/enums'; +import { + ALERT_CLUSTER_HEALTH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; +import { AlertMessageTokenType, AlertClusterHealthType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchClusterHealth } from '../lib/alerts/fetch_cluster_health'; const RED_STATUS_MESSAGE = i18n.translate('xpack.monitoring.alerts.clusterHealth.redMessage', { defaultMessage: 'Allocate missing primary and replica shards', @@ -37,12 +47,6 @@ export class ClusterHealthAlert extends BaseAlert { super(rawAlert, { id: ALERT_CLUSTER_HEALTH, name: LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label, - legacy: { - watchName: 'elasticsearch_cluster_status', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.clusterHealth.nodeNameLabel', { - defaultMessage: 'Elasticsearch cluster alert', - }), - }, actionVariables: [ { name: 'clusterHealth', @@ -58,15 +62,36 @@ export class ClusterHealthAlert extends BaseAlert { }); } - private getHealth(legacyAlert: LegacyAlert) { - return legacyAlert.prefix - .replace('Elasticsearch cluster status is ', '') - .slice(0, -1) as AlertClusterHealthType; + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const healths = await fetchClusterHealth(callCluster, clusters, esIndexPattern); + return healths.map((clusterHealth) => { + const shouldFire = clusterHealth.health !== AlertClusterHealthType.Green; + const severity = + clusterHealth.health === AlertClusterHealthType.Red + ? AlertSeverity.Danger + : AlertSeverity.Warning; + + return { + shouldFire, + severity, + meta: clusterHealth, + clusterUuid: clusterHealth.clusterUuid, + ccs: clusterHealth.ccs, + }; + }); } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const health = this.getHealth(legacyAlert); + const { health } = item.meta as AlertClusterHealth; return { text: i18n.translate('xpack.monitoring.alerts.clusterHealth.ui.firingMessage', { defaultMessage: `Elasticsearch cluster health is {health}.`, @@ -98,52 +123,56 @@ export class ClusterHealthAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const health = this.getHealth(legacyAlert); - if (alertState.ui.isFiring) { - const actionText = - health === AlertClusterHealthType.Red - ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { - defaultMessage: `Allocate missing primary and replica shards.`, - }) - : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { - defaultMessage: `Allocate missing replica shards.`, - }); - - const action = `[${actionText}](elasticsearch/indices)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, - values: { - clusterName: cluster.clusterName, - health, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', - { - defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, - values: { - clusterName: cluster.clusterName, - health, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterHealth: health, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, - }); + if (alertStates.length === 0) { + return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { health } = state.meta as AlertClusterHealth; + const actionText = + health === AlertClusterHealthType.Red + ? i18n.translate('xpack.monitoring.alerts.clusterHealth.action.danger', { + defaultMessage: `Allocate missing primary and replica shards.`, + }) + : i18n.translate('xpack.monitoring.alerts.clusterHealth.action.warning', { + defaultMessage: `Allocate missing replica shards.`, + }); + + const action = `[${actionText}](elasticsearch/indices)`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {actionText}`, + values: { + clusterName: cluster.clusterName, + health, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage', + { + defaultMessage: `Cluster health alert is firing for {clusterName}. Current health is {health}. {action}`, + values: { + clusterName: cluster.clusterName, + health, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterHealth: health, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 5f9ea3a18b570..a231cec762191 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -7,13 +7,13 @@ import { ElasticsearchVersionMismatchAlert } from './elasticsearch_version_mismatch_alert'; import { ALERT_ELASTICSEARCH_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_elasticsearch_versions', () => ({ + fetchElasticsearchVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -67,16 +68,16 @@ describe('ElasticsearchVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Elasticsearch.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const elasticsearchVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -98,8 +99,8 @@ describe('ElasticsearchVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchElasticsearchVersions as jest.Mock).mockImplementation(() => { + return elasticsearchVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -125,13 +126,19 @@ describe('ElasticsearchVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Elasticsearch node alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: - 'Multiple versions of Elasticsearch ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Elasticsearch (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -141,21 +148,26 @@ describe('ElasticsearchVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View nodes](elasticsearch/nodes)', + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: - 'Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running [8.0.0, 7.2.1]. [View nodes](elasticsearch/nodes)', + internalFullMessage: `Elasticsearch version mismatch alert is firing for testCluster. Elasticsearch is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/elasticsearch/nodes?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Elasticsearch version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if there is no mismatch', async () => { + (fetchElasticsearchVersions as jest.Mock).mockImplementation(() => { + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new ElasticsearchVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index 717d803084c6f..e8e93e4b3afec 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -12,29 +12,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_ELASTICSEARCH_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_ELASTICSEARCH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchElasticsearchVersions } from '../lib/alerts/fetch_elasticsearch_versions'; export class ElasticsearchVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_ELASTICSEARCH_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label, - legacy: { - watchName: 'elasticsearch_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Elasticsearch node alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -51,15 +51,42 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const elasticsearchVersions = await fetchElasticsearchVersions( + callCluster, + clusters, + esIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return elasticsearchVersions.map((elasticsearchVersion) => { + return { + shouldFire: elasticsearchVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: elasticsearchVersion, + clusterUuid: elasticsearchVersion.clusterUuid, + ccs: elasticsearchVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate( 'xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Elasticsearch ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, } ); @@ -71,54 +98,63 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + if (alertStates.length === 0) { + return; + } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'elasticsearch/nodes', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'Verify you have the same version across all nodes.', + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction', + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', { - defaultMessage: 'View nodes', + defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, } - ); - const action = `[${fullActionText}](elasticsearch/nodes)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Elasticsearch version mismatch alert is firing for {clusterName}. Elasticsearch is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index a6cc7445cb764..6252fc59ba246 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -7,13 +7,13 @@ import { KibanaVersionMismatchAlert } from './kibana_version_mismatch_alert'; import { ALERT_KIBANA_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_kibana_versions', () => ({ + fetchKibanaVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -70,16 +71,16 @@ describe('KibanaVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Kibana.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const kibanaVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -101,8 +102,8 @@ describe('KibanaVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchKibanaVersions as jest.Mock).mockImplementation(() => { + return kibanaVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -127,12 +128,19 @@ describe('KibanaVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Kibana instance alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: 'Multiple versions of Kibana ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Kibana (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -142,21 +150,26 @@ describe('KibanaVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View instances](kibana/instances)', + action: `[View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all instances.', - internalFullMessage: - 'Kibana version mismatch alert is firing for testCluster. Kibana is running [8.0.0, 7.2.1]. [View instances](kibana/instances)', + internalFullMessage: `Kibana version mismatch alert is firing for testCluster. Kibana is running 8.0.0, 7.2.1. [View instances](UNIT_TEST_URL/app/monitoring#/kibana/instances?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Kibana version mismatch alert is firing for testCluster. Verify you have the same version across all instances.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if there is no mismatch', async () => { + (fetchKibanaVersions as jest.Mock).mockImplementation(() => { + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new KibanaVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index 4fe71e7c27146..f1f8959787003 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -12,29 +12,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_KIBANA_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_KIBANA_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_KIBANA, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchKibanaVersions } from '../lib/alerts/fetch_kibana_versions'; export class KibanaVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_KIBANA_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label, - legacy: { - watchName: 'kibana_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Kibana instance alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -64,13 +64,40 @@ export class KibanaVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let kibanaIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_KIBANA); + if (availableCcs) { + kibanaIndexPattern = getCcsIndexPattern(kibanaIndexPattern, availableCcs); + } + const kibanaVersions = await fetchKibanaVersions( + callCluster, + clusters, + kibanaIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return kibanaVersions.map((kibanaVersion) => { + return { + shouldFire: kibanaVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: kibanaVersion, + clusterUuid: kibanaVersion.clusterUuid, + ccs: kibanaVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate('xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Kibana ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, }); @@ -81,54 +108,64 @@ export class KibanaVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', - { - defaultMessage: 'Verify you have the same version across all instances.', - } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + if (alertStates.length === 0) { + return; + } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all instances.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.fullAction', + { + defaultMessage: 'View instances', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'kibana/instances', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + const internalFullMessage = i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', + { + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, + } + ); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'View instances', + defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const action = `[${fullActionText}](kibana/instances)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Kibana version mismatch alert is firing for {clusterName}. Kibana is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + internalFullMessage, + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index fa2740eb9aa1e..0d1c1d20097e5 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -7,23 +7,20 @@ import { LicenseExpirationAlert } from './license_expiration_alert'; import { ALERT_LICENSE_EXPIRATION } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { AlertSeverity } from '../../common/enums'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_licenses', () => ({ + fetchLicenses: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), })); jest.mock('moment', () => { - const moment = function () { - return { - format: () => 'THE_DATE', - }; - }; + const moment = function () {}; moment.duration = () => ({ humanize: () => 'HUMANIZED_DURATION' }); return moment; }); @@ -76,15 +73,11 @@ describe('LicenseExpirationAlert', () => { const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: - 'The license for this cluster expires in {{#relativeTime}}metadata.time{{/relativeTime}} at {{#absoluteTime}}metadata.time{{/absoluteTime}}.', - message: 'Update your license.', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, - time: 1, - }, + const license = { + status: 'expired', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 59, + clusterUuid, }; const replaceState = jest.fn(); @@ -107,8 +100,8 @@ describe('LicenseExpirationAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [license]; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -134,7 +127,15 @@ describe('LicenseExpirationAlert', () => { { cluster: { clusterUuid, clusterName }, ccs: undefined, - nodeName: 'Elasticsearch cluster alert', + itemLabel: undefined, + meta: { + clusterUuid: 'abc123', + expiryDateMS: 5097600000, + status: 'expired', + type: 'gold', + }, + nodeId: undefined, + nodeName: undefined, ui: { isFiring: true, message: { @@ -146,14 +147,14 @@ describe('LicenseExpirationAlert', () => { type: 'time', isRelative: true, isAbsolute: false, - timestamp: 1, + timestamp: 5097600000, }, { startToken: '#absolute', type: 'time', isAbsolute: true, isRelative: false, - timestamp: 1, + timestamp: 5097600000, }, { startToken: '#start_link', @@ -163,7 +164,7 @@ describe('LicenseExpirationAlert', () => { }, ], }, - severity: 'warning', + severity: 'danger', triggeredMS: 1, lastCheckedMS: 0, }, @@ -183,9 +184,16 @@ describe('LicenseExpirationAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if the license is not expired', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 61, + clusterUuid, + }, + ]; }); const alert = new LicenseExpirationAlert(); const type = alert.getAlertType(); @@ -197,5 +205,47 @@ describe('LicenseExpirationAlert', () => { expect(replaceState).not.toHaveBeenCalledWith({}); expect(scheduleActions).not.toHaveBeenCalled(); }); + + it('should use danger severity for a license expiring soon', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 2, + clusterUuid, + }, + ]; + }); + const alert = new LicenseExpirationAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Danger); + }); + + it('should use warning severity for a license expiring in a bit', async () => { + (fetchLicenses as jest.Mock).mockImplementation(() => { + return [ + { + status: 'active', + type: 'gold', + expiryDateMS: 1000 * 60 * 60 * 24 * 31, + clusterUuid, + }, + ]; + }); + const alert = new LicenseExpirationAlert(); + const type = alert.getAlertType(); + await type.executor({ + ...executorOptions, + // @ts-ignore + params: alert.alertOptions.defaultParams, + } as any); + expect(replaceState.mock.calls[0][0].alertStates[0].ui.severity).toBe(AlertSeverity.Warning); + }); }); }); diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index cd59fa63f3b2e..24fbd98ef2e8b 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { BaseAlert } from './base_alert'; @@ -15,26 +14,32 @@ import { AlertMessage, AlertMessageTimeToken, AlertMessageLinkToken, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertLicense, + AlertLicenseState, } from '../../common/types/alerts'; import { AlertExecutorOptions, AlertInstance } from '../../../alerts/server'; -import { ALERT_LICENSE_EXPIRATION, LEGACY_ALERT_DETAILS } from '../../common/constants'; -import { AlertMessageTokenType } from '../../common/enums'; +import { + ALERT_LICENSE_EXPIRATION, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; +import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; + +const EXPIRES_DAYS = [60, 30, 14, 7]; export class LicenseExpirationAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_LICENSE_EXPIRATION, name: LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label, - legacy: { - watchName: 'xpack_license_expiration', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.licenseExpiration.nodeNameLabel', { - defaultMessage: 'Elasticsearch cluster alert', - }), - }, interval: '1d', actionVariables: [ { @@ -71,8 +76,53 @@ export class LicenseExpirationAlert extends BaseAlert { return await super.execute(options); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const licenses = await fetchLicenses(callCluster, clusters, esIndexPattern); + + return licenses.map((license) => { + const { clusterUuid, type, expiryDateMS, status, ccs } = license; + let isExpired = false; + let severity = AlertSeverity.Success; + + if (status !== 'active') { + isExpired = true; + severity = AlertSeverity.Danger; + } else if (expiryDateMS) { + for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { + if (type === 'trial' && i < 2) { + break; + } + + const fromNow = +new Date() + EXPIRES_DAYS[i] * 1000 * 60 * 60 * 24; + if (fromNow >= expiryDateMS) { + isExpired = true; + severity = i < 1 ? AlertSeverity.Warning : AlertSeverity.Danger; + break; + } + } + } + + return { + shouldFire: isExpired, + severity, + meta: license, + clusterUuid, + ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; + const license = item.meta as AlertLicense; return { text: i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { defaultMessage: `The license for this cluster expires in #relative at #absolute. #start_linkPlease update your license.#end_link`, @@ -83,14 +133,14 @@ export class LicenseExpirationAlert extends BaseAlert { type: AlertMessageTokenType.Time, isRelative: true, isAbsolute: false, - timestamp: legacyAlert.metadata.time, + timestamp: license.expiryDateMS, } as AlertMessageTimeToken, { startToken: '#absolute', type: AlertMessageTokenType.Time, isAbsolute: true, isRelative: false, - timestamp: legacyAlert.metadata.time, + timestamp: license.expiryDateMS, } as AlertMessageTimeToken, { startToken: '#start_link', @@ -104,48 +154,51 @@ export class LicenseExpirationAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const $expiry = moment(legacyAlert.metadata.time); - const $duration = moment.duration(+new Date() - $expiry.valueOf()); - if (alertState.ui.isFiring) { - const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { - defaultMessage: 'Please update your license.', - }); - const action = `[${actionText}](elasticsearch/nodes)`; - const expiredDate = $duration.humanize(); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - actionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', - { - defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, - values: { - clusterName: cluster.clusterName, - expiredDate, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - expiredDate, - clusterName: cluster.clusterName, - action, - actionPlain: actionText, - }); + if (alertStates.length === 0) { + return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state: AlertLicenseState = alertStates[0] as AlertLicenseState; + const $duration = moment.duration(+new Date() - state.expiryDateMS); + const actionText = i18n.translate('xpack.monitoring.alerts.licenseExpiration.action', { + defaultMessage: 'Please update your license.', + }); + const action = `[${actionText}](elasticsearch/nodes)`; + const expiredDate = $duration.humanize(); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {actionText}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + actionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage', + { + defaultMessage: `License expiration alert is firing for {clusterName}. Your license expires in {expiredDate}. {action}`, + values: { + clusterName: cluster.clusterName, + expiredDate, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + expiredDate, + clusterName: cluster.clusterName, + action, + actionPlain: actionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index 514fd71368085..50a826b36d58f 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -7,13 +7,13 @@ import { LogstashVersionMismatchAlert } from './logstash_version_mismatch_alert'; import { ALERT_LOGSTASH_VERSION_MISMATCH } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_logstash_versions', () => ({ + fetchLogstashVersions: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -22,6 +22,7 @@ jest.mock('../lib/alerts/fetch_clusters', () => ({ jest.mock('../static_globals', () => ({ Globals: { app: { + url: 'UNIT_TEST_URL', getLogger: () => ({ debug: jest.fn() }), config: { ui: { @@ -68,16 +69,16 @@ describe('LogstashVersionMismatchAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'This cluster is running with multiple versions of Logstash.', - message: 'Versions: [8.0.0, 7.2.1].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, + const logstashVersions = [ + { + versions: ['8.0.0', '7.2.1'], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -99,8 +100,8 @@ describe('LogstashVersionMismatchAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchLogstashVersions as jest.Mock).mockImplementation(() => { + return logstashVersions; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -126,12 +127,19 @@ describe('LogstashVersionMismatchAlert', () => { alertStates: [ { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, - ccs: undefined, - nodeName: 'Logstash node alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + versions: ['8.0.0', '7.2.1'], + }, ui: { isFiring: true, message: { - text: 'Multiple versions of Logstash ([8.0.0, 7.2.1]) running in this cluster.', + text: 'Multiple versions of Logstash (8.0.0, 7.2.1) running in this cluster.', }, severity: 'warning', triggeredMS: 1, @@ -141,21 +149,26 @@ describe('LogstashVersionMismatchAlert', () => { ], }); expect(scheduleActions).toHaveBeenCalledWith('default', { - action: '[View nodes](logstash/nodes)', + action: `[View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, actionPlain: 'Verify you have the same version across all nodes.', - internalFullMessage: - 'Logstash version mismatch alert is firing for testCluster. Logstash is running [8.0.0, 7.2.1]. [View nodes](logstash/nodes)', + internalFullMessage: `Logstash version mismatch alert is firing for testCluster. Logstash is running 8.0.0, 7.2.1. [View nodes](UNIT_TEST_URL/app/monitoring#/logstash/nodes?_g=(cluster_uuid:${clusterUuid}))`, internalShortMessage: 'Logstash version mismatch alert is firing for testCluster. Verify you have the same version across all nodes.', - versionList: '[8.0.0, 7.2.1]', + versionList: ['8.0.0', '7.2.1'], clusterName, state: 'firing', }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if there is no mismatch', async () => { + (fetchLogstashVersions as jest.Mock).mockImplementation(() => { + return [ + { + versions: ['8.0.0'], + clusterUuid, + ccs, + }, + ]; }); const alert = new LogstashVersionMismatchAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 0dc93743e2276..d903dd49600ad 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -12,29 +12,29 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, + AlertInstanceState, + CommonAlertParams, + AlertVersions, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_LOGSTASH_VERSION_MISMATCH, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_LOGSTASH_VERSION_MISMATCH, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_LOGSTASH, +} from '../../common/constants'; import { AlertSeverity } from '../../common/enums'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { fetchLogstashVersions } from '../lib/alerts/fetch_logstash_versions'; export class LogstashVersionMismatchAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_LOGSTASH_VERSION_MISMATCH, name: LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label, - legacy: { - watchName: 'logstash_version_mismatch', - nodeNameLabel: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel', - { - defaultMessage: 'Logstash node alert', - } - ), - changeDataValues: { severity: AlertSeverity.Warning }, - }, interval: '1d', actionVariables: [ { @@ -51,15 +51,42 @@ export class LogstashVersionMismatchAlert extends BaseAlert { }); } + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let logstashIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_LOGSTASH); + if (availableCcs) { + logstashIndexPattern = getCcsIndexPattern(logstashIndexPattern, availableCcs); + } + const logstashVersions = await fetchLogstashVersions( + callCluster, + clusters, + logstashIndexPattern, + Globals.app.config.ui.max_bucket_size + ); + + return logstashVersions.map((logstashVersion) => { + return { + shouldFire: logstashVersion.versions.length > 1, + severity: AlertSeverity.Warning, + meta: logstashVersion, + clusterUuid: logstashVersion.clusterUuid, + ccs: logstashVersion.ccs, + }; + }); + } + protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); + const { versions } = item.meta as AlertVersions; const text = i18n.translate( 'xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage', { defaultMessage: `Multiple versions of Logstash ({versions}) running in this cluster.`, values: { - versions, + versions: versions.join(', '), }, } ); @@ -71,54 +98,63 @@ export class LogstashVersionMismatchAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - const versions = this.getVersions(legacyAlert); - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + if (alertStates.length === 0) { + return; + } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0]; + const { versions } = state.meta as AlertVersions; + const shortActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.shortAction', + { + defaultMessage: 'Verify you have the same version across all nodes.', + } + ); + const fullActionText = i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + { + defaultMessage: 'View nodes', + } + ); + const globalStateLink = this.createGlobalStateLink( + 'logstash/nodes', + cluster.clusterUuid, + state.ccs + ); + const action = `[${fullActionText}](${globalStateLink})`; + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', { - defaultMessage: 'Verify you have the same version across all nodes.', + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, } - ); - const fullActionText = i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.fullAction', + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', { - defaultMessage: 'View nodes', + defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, + values: { + clusterName: cluster.clusterName, + versions: versions.join(', '), + action, + }, } - ); - const action = `[${fullActionText}](logstash/nodes)`; - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.logstashVersionMismatch.firing.internalFullMessage', - { - defaultMessage: `Logstash version mismatch alert is firing for {clusterName}. Logstash is running {versions}. {action}`, - values: { - clusterName: cluster.clusterName, - versions, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - versionList: versions, - action, - actionPlain: shortActionText, - }); - } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + versionList: versions, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 59b61645e2eca..848436573fab9 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -7,13 +7,13 @@ import { NodesChangedAlert } from './nodes_changed_alert'; import { ALERT_NODES_CHANGED } from '../../common/constants'; -import { fetchLegacyAlerts } from '../lib/alerts/fetch_legacy_alerts'; +import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; import { fetchClusters } from '../lib/alerts/fetch_clusters'; const RealDate = Date; -jest.mock('../lib/alerts/fetch_legacy_alerts', () => ({ - fetchLegacyAlerts: jest.fn(), +jest.mock('../lib/alerts/fetch_nodes_from_cluster_stats', () => ({ + fetchNodesFromClusterStats: jest.fn(), })); jest.mock('../lib/alerts/fetch_clusters', () => ({ fetchClusters: jest.fn(), @@ -73,23 +73,33 @@ describe('NodesChangedAlert', () => { function FakeDate() {} FakeDate.prototype.valueOf = () => 1; + const nodeUuid = 'myNodeUuid'; + const nodeEphemeralId = 'myEphemeralId'; + const nodeEphemeralIdChanged = 'myEphemeralIdChanged'; + const nodeName = 'test'; + const ccs = undefined; const clusterUuid = 'abc123'; const clusterName = 'testCluster'; - const legacyAlert = { - prefix: 'Elasticsearch cluster nodes have changed!', - message: 'Node was restarted [1]: [test].', - metadata: { - severity: 1000, - cluster_uuid: clusterUuid, - }, - nodes: { - added: {}, - removed: {}, - restarted: { - test: 'test', - }, + const nodes = [ + { + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + clusterUuid, + ccs, }, - }; + ]; const replaceState = jest.fn(); const scheduleActions = jest.fn(); @@ -111,8 +121,8 @@ describe('NodesChangedAlert', () => { beforeEach(() => { // @ts-ignore Date = FakeDate; - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return [legacyAlert]; + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { + return nodes; }); (fetchClusters as jest.Mock).mockImplementation(() => { return [{ clusterUuid, clusterName }]; @@ -138,8 +148,28 @@ describe('NodesChangedAlert', () => { alertStates: [ { cluster: { clusterUuid, clusterName }, - ccs: undefined, - nodeName: 'Elasticsearch nodes alert', + ccs, + itemLabel: undefined, + nodeId: undefined, + nodeName: undefined, + meta: { + ccs, + clusterUuid, + recentNodes: [ + { + nodeUuid, + nodeEphemeralId: nodeEphemeralIdChanged, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + }, ui: { isFiring: true, message: { @@ -167,9 +197,28 @@ describe('NodesChangedAlert', () => { }); }); - it('should not fire actions if there is no legacy alert', async () => { - (fetchLegacyAlerts as jest.Mock).mockImplementation(() => { - return []; + it('should not fire actions if no nodes have changed', async () => { + (fetchNodesFromClusterStats as jest.Mock).mockImplementation(() => { + return [ + { + recentNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + priorNodes: [ + { + nodeUuid, + nodeEphemeralId, + nodeName, + }, + ], + clusterUuid, + ccs, + }, + ]; }); const alert = new NodesChangedAlert(); const type = alert.getAlertType(); diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index 10dc6f911409e..63b3ef672405e 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -12,26 +12,61 @@ import { AlertCluster, AlertState, AlertMessage, - LegacyAlert, - LegacyAlertNodesChangedList, + AlertClusterStatsNodes, + AlertClusterStatsNode, + CommonAlertParams, + AlertInstanceState, + AlertNodesChangedState, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; -import { ALERT_NODES_CHANGED, LEGACY_ALERT_DETAILS } from '../../common/constants'; +import { + ALERT_NODES_CHANGED, + LEGACY_ALERT_DETAILS, + INDEX_PATTERN_ELASTICSEARCH, +} from '../../common/constants'; import { AlertingDefaults } from './alert_helpers'; import { SanitizedAlert } from '../../../alerts/common'; +import { Globals } from '../static_globals'; +import { fetchNodesFromClusterStats } from '../lib/alerts/fetch_nodes_from_cluster_stats'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { appendMetricbeatIndex } from '../lib/alerts/append_mb_index'; +import { AlertSeverity } from '../../common/enums'; + +interface AlertNodesChangedStates { + removed: AlertClusterStatsNode[]; + added: AlertClusterStatsNode[]; + restarted: AlertClusterStatsNode[]; +} + +function getNodeStates(nodes: AlertClusterStatsNodes): AlertNodesChangedStates { + const removed = nodes.priorNodes.filter( + (priorNode) => + !nodes.recentNodes.find((recentNode) => priorNode.nodeUuid === recentNode.nodeUuid) + ); + const added = nodes.recentNodes.filter( + (recentNode) => + !nodes.priorNodes.find((priorNode) => priorNode.nodeUuid === recentNode.nodeUuid) + ); + const restarted = nodes.recentNodes.filter( + (recentNode) => + nodes.priorNodes.find((priorNode) => priorNode.nodeUuid === recentNode.nodeUuid) && + !nodes.priorNodes.find( + (priorNode) => priorNode.nodeEphemeralId === recentNode.nodeEphemeralId + ) + ); + + return { + removed, + added, + restarted, + }; +} export class NodesChangedAlert extends BaseAlert { constructor(public rawAlert?: SanitizedAlert) { super(rawAlert, { id: ALERT_NODES_CHANGED, name: LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label, - legacy: { - watchName: 'elasticsearch_nodes', - nodeNameLabel: i18n.translate('xpack.monitoring.alerts.nodesChanged.nodeNameLabel', { - defaultMessage: 'Elasticsearch nodes alert', - }), - changeDataValues: { shouldFire: true }, - }, actionVariables: [ { name: 'added', @@ -65,13 +100,39 @@ export class NodesChangedAlert extends BaseAlert { }); } - private getNodeStates(legacyAlert: LegacyAlert): LegacyAlertNodesChangedList { - return legacyAlert.nodes || { added: {}, removed: {}, restarted: {} }; + protected async fetchData( + params: CommonAlertParams, + callCluster: any, + clusters: AlertCluster[], + availableCcs: string[] + ): Promise { + let esIndexPattern = appendMetricbeatIndex(Globals.app.config, INDEX_PATTERN_ELASTICSEARCH); + if (availableCcs) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + const nodesFromClusterStats = await fetchNodesFromClusterStats( + callCluster, + clusters, + esIndexPattern + ); + return nodesFromClusterStats.map((nodes) => { + const { removed, added, restarted } = getNodeStates(nodes); + const shouldFire = removed.length > 0 || added.length > 0 || restarted.length > 0; + const severity = AlertSeverity.Warning; + + return { + shouldFire, + severity, + meta: nodes, + clusterUuid: nodes.clusterUuid, + ccs: nodes.ccs, + }; + }); } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const legacyAlert = item.meta as LegacyAlert; - const states = this.getNodeStates(legacyAlert); + const nodes = item.meta as AlertClusterStatsNodes; + const states = getNodeStates(nodes); if (!alertState.ui.isFiring) { return { text: i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.resolvedMessage', { @@ -80,11 +141,7 @@ export class NodesChangedAlert extends BaseAlert { }; } - if ( - Object.values(states.added).length === 0 && - Object.values(states.removed).length === 0 && - Object.values(states.restarted).length === 0 - ) { + if (states.added.length === 0 && states.removed.length === 0 && states.restarted.length === 0) { return { text: i18n.translate( 'xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage', @@ -96,29 +153,29 @@ export class NodesChangedAlert extends BaseAlert { } const addedText = - Object.values(states.added).length > 0 + states.added.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage', { defaultMessage: `Elasticsearch nodes '{added}' added to this cluster.`, values: { - added: Object.values(states.added).join(','), + added: states.added.map((n) => n.nodeName).join(','), }, }) : null; const removedText = - Object.values(states.removed).length > 0 + states.removed.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.removedFiringMessage', { defaultMessage: `Elasticsearch nodes '{removed}' removed from this cluster.`, values: { - removed: Object.values(states.removed).join(','), + removed: states.removed.map((n) => n.nodeName).join(','), }, }) : null; const restartedText = - Object.values(states.restarted).length > 0 + states.restarted.length > 0 ? i18n.translate('xpack.monitoring.alerts.nodesChanged.ui.restartedFiringMessage', { defaultMessage: `Elasticsearch nodes '{restarted}' restarted in this cluster.`, values: { - restarted: Object.values(states.restarted).join(','), + restarted: states.restarted.map((n) => n.nodeName).join(','), }, }) : null; @@ -130,55 +187,60 @@ export class NodesChangedAlert extends BaseAlert { protected async executeActions( instance: AlertInstance, - alertState: AlertState, - item: AlertData, + { alertStates }: AlertInstanceState, + item: AlertData | null, cluster: AlertCluster ) { - const legacyAlert = item.meta as LegacyAlert; - if (alertState.ui.isFiring) { - const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { - defaultMessage: 'Verify that you added, removed, or restarted nodes.', - }); - const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { - defaultMessage: 'View nodes', - }); - const action = `[${fullActionText}](elasticsearch/nodes)`; - const states = this.getNodeStates(legacyAlert); - const added = Object.values(states.added).join(','); - const removed = Object.values(states.removed).join(','); - const restarted = Object.values(states.restarted).join(','); - instance.scheduleActions('default', { - internalShortMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, - values: { - clusterName: cluster.clusterName, - shortActionText, - }, - } - ), - internalFullMessage: i18n.translate( - 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', - { - defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, - values: { - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - }, - } - ), - state: AlertingDefaults.ALERT_STATE.firing, - clusterName: cluster.clusterName, - added, - removed, - restarted, - action, - actionPlain: shortActionText, - }); + if (alertStates.length === 0) { + return; } + + // Logic in the base alert assumes that all alerts will operate against multiple nodes/instances (such as a CPU alert against ES nodes) + // However, some alerts operate on the state of the cluster itself and are only concerned with a single state + const state = alertStates[0] as AlertNodesChangedState; + const nodes = state.meta as AlertClusterStatsNodes; + const shortActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.shortAction', { + defaultMessage: 'Verify that you added, removed, or restarted nodes.', + }); + const fullActionText = i18n.translate('xpack.monitoring.alerts.nodesChanged.fullAction', { + defaultMessage: 'View nodes', + }); + const action = `[${fullActionText}](elasticsearch/nodes)`; + const states = getNodeStates(nodes); + const added = states.added.map((node) => node.nodeName).join(','); + const removed = states.removed.map((node) => node.nodeName).join(','); + const restarted = states.restarted.map((node) => node.nodeName).join(','); + instance.scheduleActions('default', { + internalShortMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. {shortActionText}`, + values: { + clusterName: cluster.clusterName, + shortActionText, + }, + } + ), + internalFullMessage: i18n.translate( + 'xpack.monitoring.alerts.nodesChanged.firing.internalFullMessage', + { + defaultMessage: `Nodes changed alert is firing for {clusterName}. The following Elasticsearch nodes have been added:{added} removed:{removed} restarted:{restarted}. {action}`, + values: { + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + }, + } + ), + state: AlertingDefaults.ALERT_STATE.firing, + clusterName: cluster.clusterName, + added, + removed, + restarted, + action, + actionPlain: shortActionText, + }); } } diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts new file mode 100644 index 0000000000000..2fdbbe80b7e89 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fetchClusterHealth } from './fetch_cluster_health'; + +describe('fetchClusterHealth', () => { + it('should return the cluster health', async () => { + const status = 'green'; + const clusterUuid = 'sdfdsaj34434'; + const callCluster = jest.fn(() => ({ + hits: { + hits: [ + { + _index: '.monitoring-es-7', + _source: { + cluster_state: { + status, + }, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + + const clusters = [{ clusterUuid, clusterName: 'foo' }]; + const index = '.monitoring-es-*'; + + const health = await fetchClusterHealth(callCluster, clusters, index); + expect(health).toEqual([ + { + health: status, + clusterUuid, + ccs: undefined, + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts new file mode 100644 index 0000000000000..bcfa2da0958a2 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_cluster_health.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertCluster, AlertClusterHealth } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +export async function fetchClusterHealth( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_state.status', + 'hits.hits._source.cluster_uuid', + 'hits.hits._index', + ], + body: { + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { + return { + health: hit._source.cluster_state?.status, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index.includes(':') ? hit._index.split(':')[0] : undefined, + } as AlertClusterHealth; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts new file mode 100644 index 0000000000000..e4f4a4d364ebf --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fetchElasticsearchVersions } from './fetch_elasticsearch_versions'; + +describe('fetchElasticsearchVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-es-*'; + const size = 10; + const versions = ['8.0.0', '7.2.1']; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + hits: { + hits: [ + { + _index: `Monitoring:${index}`, + _source: { + cluster_uuid: 'cluster123', + cluster_stats: { + nodes: { + versions, + }, + }, + }, + }, + ], + }, + }; + }); + + const result = await fetchElasticsearchVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions, + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts new file mode 100644 index 0000000000000..373ddb62aaee8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_elasticsearch_versions.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +export async function fetchElasticsearchVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.cluster_stats.nodes.versions', + 'hits.hits._index', + 'hits.hits._source.cluster_uuid', + ], + body: { + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response = await callCluster('search', params); + return response.hits.hits.map((hit: { _source: ElasticsearchSource; _index: string }) => { + const versions = hit._source.cluster_stats?.nodes?.versions; + return { + versions, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index.includes(':') ? hit._index.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts new file mode 100644 index 0000000000000..518828ef0b1c8 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fetchKibanaVersions } from './fetch_kibana_versions'; + +describe('fetchKibanaVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-kibana-*'; + const size = 10; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + aggregations: { + index: { + buckets: [ + { + key: `Monitoring:${index}`, + }, + ], + }, + cluster: { + buckets: [ + { + key: 'cluster123', + group_by_kibana: { + buckets: [ + { + group_by_version: { + buckets: [ + { + key: '8.0.0', + }, + ], + }, + }, + { + group_by_version: { + buckets: [ + { + key: '7.2.1', + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + const result = await fetchKibanaVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions: ['8.0.0', '7.2.1'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts new file mode 100644 index 0000000000000..2e7fe192df656 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_kibana_versions.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { get } from 'lodash'; +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; + +interface ESAggResponse { + key: string; +} + +export async function fetchKibanaVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'kibana_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + cluster: { + terms: { + field: 'cluster_uuid', + size: 1, + }, + aggs: { + group_by_kibana: { + terms: { + field: 'kibana_stats.kibana.uuid', + size, + }, + aggs: { + group_by_version: { + terms: { + field: 'kibana_stats.kibana.version', + size: 1, + order: { + latest_report: 'desc', + }, + }, + aggs: { + latest_report: { + max: { + field: 'timestamp', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const indexName = get(response, 'aggregations.index.buckets[0].key', ''); + const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; + return clusterList.map((cluster) => { + const clusterUuid = cluster.key; + const uuids = get(cluster, 'group_by_kibana.buckets', []); + const byVersion: { [version: string]: boolean } = {}; + for (const uuid of uuids) { + const version = get(uuid, 'group_by_version.buckets[0].key', ''); + if (!version) { + continue; + } + byVersion[version] = true; + } + return { + versions: Object.keys(byVersion), + clusterUuid, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts deleted file mode 100644 index 086c5c7da9139..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts +++ /dev/null @@ -1,96 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { fetchLegacyAlerts } from './fetch_legacy_alerts'; - -describe('fetchLegacyAlerts', () => { - let callCluster = jest.fn(); - const clusters = [ - { - clusterUuid: 'abc123', - clusterName: 'test', - }, - ]; - const index = '.monitoring-es-*'; - const size = 10; - - it('fetch legacy alerts', async () => { - const prefix = 'thePrefix'; - const message = 'theMessage'; - const nodes = {}; - const metadata = { - severity: 2000, - cluster_uuid: clusters[0].clusterUuid, - metadata: {}, - }; - callCluster = jest.fn().mockImplementation(() => { - return { - hits: { - hits: [ - { - _source: { - prefix, - message, - nodes, - metadata, - }, - }, - ], - }, - }; - }); - const result = await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); - expect(result).toEqual([ - { - message, - metadata, - nodes, - nodeName: '', - prefix, - }, - ]); - }); - - it('should use consistent params', async () => { - let params = null; - callCluster = jest.fn().mockImplementation((...args) => { - params = args[1]; - }); - await fetchLegacyAlerts(callCluster, clusters, index, 'myWatch', size); - expect(params).toStrictEqual({ - index, - filterPath: [ - 'hits.hits._source.prefix', - 'hits.hits._source.message', - 'hits.hits._source.resolved_timestamp', - 'hits.hits._source.nodes', - 'hits.hits._source.metadata.*', - ], - body: { - size, - sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }], - query: { - bool: { - minimum_should_match: 1, - filter: [ - { - terms: { 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid) }, - }, - { term: { 'metadata.watch': 'myWatch' } }, - ], - should: [ - { range: { timestamp: { gte: 'now-2m' } } }, - { range: { resolved_timestamp: { gte: 'now-2m' } } }, - { bool: { must_not: { exists: { field: 'resolved_timestamp' } } } }, - ], - }, - }, - collapse: { field: 'metadata.cluster_uuid' }, - }, - }); - }); -}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts deleted file mode 100644 index 96438da111b6d..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts +++ /dev/null @@ -1,97 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { get } from 'lodash'; -import { LegacyAlert, AlertCluster, LegacyAlertMetadata } from '../../../common/types/alerts'; - -export async function fetchLegacyAlerts( - callCluster: any, - clusters: AlertCluster[], - index: string, - watchName: string, - size: number -): Promise { - const params = { - index, - filterPath: [ - 'hits.hits._source.prefix', - 'hits.hits._source.message', - 'hits.hits._source.resolved_timestamp', - 'hits.hits._source.nodes', - 'hits.hits._source.metadata.*', - ], - body: { - size, - sort: [ - { - timestamp: { - order: 'desc', - unmapped_type: 'long', - }, - }, - ], - query: { - bool: { - minimum_should_match: 1, - filter: [ - { - terms: { - 'metadata.cluster_uuid': clusters.map((cluster) => cluster.clusterUuid), - }, - }, - { - term: { - 'metadata.watch': watchName, - }, - }, - ], - should: [ - { - range: { - timestamp: { - gte: 'now-2m', - }, - }, - }, - { - range: { - resolved_timestamp: { - gte: 'now-2m', - }, - }, - }, - { - bool: { - must_not: { - exists: { - field: 'resolved_timestamp', - }, - }, - }, - }, - ], - }, - }, - collapse: { - field: 'metadata.cluster_uuid', - }, - }, - }; - - const response = await callCluster('search', params); - return get(response, 'hits.hits', []).map((hit: any) => { - const legacyAlert: LegacyAlert = { - prefix: get(hit, '_source.prefix'), - message: get(hit, '_source.message'), - resolved_timestamp: get(hit, '_source.resolved_timestamp'), - nodes: get(hit, '_source.nodes'), - nodeName: '', // This is set by BaseAlert - metadata: get(hit, '_source.metadata') as LegacyAlertMetadata, - }; - return legacyAlert; - }); -} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts new file mode 100644 index 0000000000000..715c8c50a45e7 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { fetchLicenses } from './fetch_licenses'; + +describe('fetchLicenses', () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + + it('return a list of licenses', async () => { + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + }, + ]); + }); + + it('should only search for the clusters provided', async () => { + const callCluster = jest.fn(); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); + }); + + it('should limit the time period in the query', async () => { + const callCluster = jest.fn(); + const clusters = [{ clusterUuid, clusterName }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts new file mode 100644 index 0000000000000..6cec7f3296926 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertLicense, AlertCluster } from '../../../common/types/alerts'; +import { ElasticsearchResponse } from '../../../common/types/es'; + +export async function fetchLicenses( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.license.*', + 'hits.hits._source.cluster_uuid', + 'hits.hits._index', + ], + body: { + size: clusters.length, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + collapse: { + field: 'cluster_uuid', + }, + }, + }; + + const response: ElasticsearchResponse = await callCluster('search', params); + return ( + response?.hits?.hits.map((hit) => { + const rawLicense = hit._source.license ?? {}; + const license: AlertLicense = { + status: rawLicense.status ?? '', + type: rawLicense.type ?? '', + expiryDateMS: rawLicense.expiry_date_in_millis ?? 0, + clusterUuid: hit._source.cluster_uuid, + ccs: hit._index, + }; + return license; + }) ?? [] + ); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts new file mode 100644 index 0000000000000..a739593df27e9 --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fetchLogstashVersions } from './fetch_logstash_versions'; + +describe('fetchLogstashVersions', () => { + let callCluster = jest.fn(); + const clusters = [ + { + clusterUuid: 'cluster123', + clusterName: 'test-cluster', + }, + ]; + const index = '.monitoring-logstash-*'; + const size = 10; + + it('fetch as expected', async () => { + callCluster = jest.fn().mockImplementation(() => { + return { + aggregations: { + index: { + buckets: [ + { + key: `Monitoring:${index}`, + }, + ], + }, + cluster: { + buckets: [ + { + key: 'cluster123', + group_by_logstash: { + buckets: [ + { + group_by_version: { + buckets: [ + { + key: '8.0.0', + }, + ], + }, + }, + { + group_by_version: { + buckets: [ + { + key: '7.2.1', + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }; + }); + + const result = await fetchLogstashVersions(callCluster, clusters, index, size); + expect(result).toEqual([ + { + clusterUuid: clusters[0].clusterUuid, + ccs: 'Monitoring', + versions: ['8.0.0', '7.2.1'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts new file mode 100644 index 0000000000000..8f20c64d6243e --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_logstash_versions.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { get } from 'lodash'; +import { AlertCluster, AlertVersions } from '../../../common/types/alerts'; + +interface ESAggResponse { + key: string; +} + +export async function fetchLogstashVersions( + callCluster: any, + clusters: AlertCluster[], + index: string, + size: number +): Promise { + const params = { + index, + filterPath: ['aggregations'], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map((cluster) => cluster.clusterUuid), + }, + }, + { + term: { + type: 'logstash_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 1, + }, + }, + cluster: { + terms: { + field: 'cluster_uuid', + size: 1, + }, + aggs: { + group_by_logstash: { + terms: { + field: 'logstash_stats.logstash.uuid', + size, + }, + aggs: { + group_by_version: { + terms: { + field: 'logstash_stats.logstash.version', + size: 1, + order: { + latest_report: 'desc', + }, + }, + aggs: { + latest_report: { + max: { + field: 'timestamp', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const indexName = get(response, 'aggregations.index.buckets[0].key', ''); + const clusterList = get(response, 'aggregations.cluster.buckets', []) as ESAggResponse[]; + return clusterList.map((cluster) => { + const clusterUuid = cluster.key; + const uuids = get(cluster, 'group_by_logstash.buckets', []); + const byVersion: { [version: string]: boolean } = {}; + for (const uuid of uuids) { + const version = get(uuid, 'group_by_version.buckets[0].key', ''); + if (!version) { + continue; + } + byVersion[version] = true; + } + return { + versions: Object.keys(byVersion), + clusterUuid, + ccs: indexName.includes(':') ? indexName.split(':')[0] : null, + }; + }); +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts new file mode 100644 index 0000000000000..c399594c170fa --- /dev/null +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_nodes_from_cluster_stats.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { AlertCluster, AlertClusterStatsNodes } from '../../../common/types/alerts'; +import { ElasticsearchSource } from '../../../common/types/es'; + +function formatNode( + nodes: NonNullable['nodes']> | undefined +) { + if (!nodes) { + return []; + } + return Object.keys(nodes).map((nodeUuid) => { + return { + nodeUuid, + nodeEphemeralId: nodes[nodeUuid].ephemeral_id, + nodeName: nodes[nodeUuid].name, + }; + }); +} + +export async function fetchNodesFromClusterStats( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: ['aggregations.clusters.buckets'], + body: { + size: 0, + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + query: { + bool: { + filter: [ + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + include: clusters.map((cluster) => cluster.clusterUuid), + field: 'cluster_uuid', + }, + aggs: { + top: { + top_hits: { + sort: [ + { + timestamp: { + order: 'desc', + unmapped_type: 'long', + }, + }, + ], + _source: { + includes: ['cluster_state.nodes_hash', 'cluster_state.nodes'], + }, + size: 2, + }, + }, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + const nodes = []; + const clusterBuckets = response.aggregations.clusters.buckets; + for (const clusterBucket of clusterBuckets) { + const clusterUuid = clusterBucket.key; + const hits = clusterBucket.top.hits.hits; + const indexName = hits[0]._index; + nodes.push({ + clusterUuid, + recentNodes: formatNode(hits[0]._source.cluster_state?.nodes), + priorNodes: formatNode(hits[1]._source.cluster_state?.nodes), + ccs: indexName.includes(':') ? indexName.split(':')[0] : undefined, + }); + } + return nodes; +} diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 6c08a0b3db758..399b26a6c5c31 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -28,7 +28,7 @@ export async function fetchStatus( await Promise.all( (alertTypes || ALERTS).map(async (type) => { const alert = await AlertsFactory.getByType(type, alertsClient); - if (!alert || !alert.isEnabled(licenseService) || !alert.rawAlert) { + if (!alert || !alert.rawAlert) { return; } diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index a8389e26d4f9f..901ea96d525e8 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -25,8 +25,7 @@ export function enableAlertsRoute(_server: unknown, npRoute: RouteDependencies) }, async (context, request, response) => { try { - const alerts = AlertsFactory.getAll().filter((a) => a.isEnabled(npRoute.licenseService)); - + const alerts = AlertsFactory.getAll(); if (alerts.length) { const { isSufficientlySecure, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6658671b84682..168eb14966493 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14427,7 +14427,6 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。現在の正常性は{health}です。{action}", "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "クラスター正常性アラートが{clusterName}に対して作動しています。現在の正常性は{health}です。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "クラスターの正常性", - "xpack.monitoring.alerts.clusterHealth.nodeNameLabel": "Elasticsearch クラスターアラート", "xpack.monitoring.alerts.clusterHealth.redMessage": "見つからないプライマリおよびレプリカシャードを割り当て", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearchクラスターの正常性は{health}です。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}. #start_linkView now#end_link", @@ -14467,7 +14466,6 @@ "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "{clusterName}に対してElasticsearchバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearch バージョン不一致", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel": "Elasticsearch ノードアラート", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Elasticsearch({versions})が実行されています。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, other {日}}", @@ -14481,7 +14479,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "{clusterName}に対してKibanaバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "インスタンスを表示", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana バージョン不一致", - "xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel": "Kibana インスタンスアラート", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "すべてのインスタンスのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Kibana({versions})が実行されています。", "xpack.monitoring.alerts.legacyAlert.expressionText": "構成するものがありません。", @@ -14492,7 +14489,6 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "ライセンス有効期限アラートが {clusterName} に対して実行されています。ライセンスは{expiredDate}に期限切れになります。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "ライセンス有効期限アラートが {clusterName} に対して実行されています。ライセンスは{expiredDate}に期限切れになります。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.nodeNameLabel": "Elasticsearch クラスターアラート", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは#absoluteの#relativeに期限切れになります。#start_linkライセンスを更新してください。#end_link", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "このクラスターを実行している Logstash のバージョン。", "xpack.monitoring.alerts.logstashVersionMismatch.description": "クラスターに複数のバージョンの Logstash があるときにアラートを発行します。", @@ -14500,7 +14496,6 @@ "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "{clusterName}に対してLogstashバージョン不一致アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "ノードの表示", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash バージョン不一致", - "xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel": "Logstash ノードアラート", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "すべてのノードのバージョンが同じことを確認してください。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "このクラスターでは、複数のバージョンの Logstash({versions})が実行されています。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "高メモリー使用率を報告しているノード数。", @@ -14543,7 +14538,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "{clusterName}に対してノード変更アラートが実行されています。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "ノードの表示", "xpack.monitoring.alerts.nodesChanged.label": "ノードが変更されました", - "xpack.monitoring.alerts.nodesChanged.nodeNameLabel": "Elasticsearch ノードアラート", "xpack.monitoring.alerts.nodesChanged.shortAction": "ノードを追加、削除、または再起動したことを確認してください。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearchノード「{added}」がこのクラスターに追加されました。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearchノードが変更されました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9602583e8d215..129deb575a52f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14469,7 +14469,6 @@ "xpack.monitoring.alerts.clusterHealth.firing.internalFullMessage": "为 {clusterName} 触发了集群运行状况告警。当前运行状况为 {health}。{action}", "xpack.monitoring.alerts.clusterHealth.firing.internalShortMessage": "为 {clusterName} 触发了集群运行状况告警。当前运行状况为 {health}。{actionText}", "xpack.monitoring.alerts.clusterHealth.label": "集群运行状况", - "xpack.monitoring.alerts.clusterHealth.nodeNameLabel": "Elasticsearch 集群告警", "xpack.monitoring.alerts.clusterHealth.redMessage": "分配缺失的主分片和副本分片", "xpack.monitoring.alerts.clusterHealth.ui.firingMessage": "Elasticsearch 集群运行状况为 {health}。", "xpack.monitoring.alerts.clusterHealth.ui.nextSteps.message1": "{message}。#start_link立即查看#end_link", @@ -14509,7 +14508,6 @@ "xpack.monitoring.alerts.elasticsearchVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Elasticsearch 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.elasticsearchVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.elasticsearchVersionMismatch.label": "Elasticsearch 版本不匹配", - "xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel": "Elasticsearch 节点告警", "xpack.monitoring.alerts.elasticsearchVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.elasticsearchVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Elasticsearch ({versions}) 版本。", "xpack.monitoring.alerts.flyoutExpressions.timeUnits.dayLabel": "{timeValue, plural, other {天}}", @@ -14523,7 +14521,6 @@ "xpack.monitoring.alerts.kibanaVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Kibana 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.kibanaVersionMismatch.fullAction": "查看实例", "xpack.monitoring.alerts.kibanaVersionMismatch.label": "Kibana 版本不匹配", - "xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel": "Kibana 实例告警", "xpack.monitoring.alerts.kibanaVersionMismatch.shortAction": "确认所有实例具有相同的版本。", "xpack.monitoring.alerts.kibanaVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Kibana 版本 ({versions})。", "xpack.monitoring.alerts.legacyAlert.expressionText": "没有可配置的内容。", @@ -14534,7 +14531,6 @@ "xpack.monitoring.alerts.licenseExpiration.firing.internalFullMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{action}", "xpack.monitoring.alerts.licenseExpiration.firing.internalShortMessage": "为 {clusterName} 触发了许可证到期告警。您的许可证将于 {expiredDate}到期。{actionText}", "xpack.monitoring.alerts.licenseExpiration.label": "许可证到期", - "xpack.monitoring.alerts.licenseExpiration.nodeNameLabel": "Elasticsearch 集群告警", "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将于 #relative后,即 #absolute到期。 #start_link请更新您的许可证。#end_link", "xpack.monitoring.alerts.logstashVersionMismatch.actionVariables.clusterHealth": "此集群中运行的 Logstash 版本。", "xpack.monitoring.alerts.logstashVersionMismatch.description": "集群包含多个版本的 Logstash 时告警。", @@ -14542,7 +14538,6 @@ "xpack.monitoring.alerts.logstashVersionMismatch.firing.internalShortMessage": "为 {clusterName} 触发了 Logstash 版本不匹配告警。{shortActionText}", "xpack.monitoring.alerts.logstashVersionMismatch.fullAction": "查看节点", "xpack.monitoring.alerts.logstashVersionMismatch.label": "Logstash 版本不匹配", - "xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel": "Logstash 节点告警", "xpack.monitoring.alerts.logstashVersionMismatch.shortAction": "确认所有节点具有相同的版本。", "xpack.monitoring.alerts.logstashVersionMismatch.ui.firingMessage": "在此集群中正运行着多个 Logstash 版本 ({versions})。", "xpack.monitoring.alerts.memoryUsage.actionVariables.count": "报告高内存使用率的节点数目。", @@ -14585,7 +14580,6 @@ "xpack.monitoring.alerts.nodesChanged.firing.internalShortMessage": "为 {clusterName} 触发了节点已更改告警。{shortActionText}", "xpack.monitoring.alerts.nodesChanged.fullAction": "查看节点", "xpack.monitoring.alerts.nodesChanged.label": "节点已更改", - "xpack.monitoring.alerts.nodesChanged.nodeNameLabel": "Elasticsearch 节点告警", "xpack.monitoring.alerts.nodesChanged.shortAction": "确认您已添加、移除或重新启动节点。", "xpack.monitoring.alerts.nodesChanged.ui.addedFiringMessage": "Elasticsearch 节点“{added}”已添加到此集群。", "xpack.monitoring.alerts.nodesChanged.ui.nothingDetectedFiringMessage": "Elasticsearch 节点已更改", From c7e23bffc681805460b6b2531723cae0d0d7b79e Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 8 Feb 2021 21:51:42 -0500 Subject: [PATCH 16/19] [Uptime] Migrate to TypeScript project references (#90510) * Add reference to Uptime plugin to root tsconfig.refs.json. * Add Uptime path to excluded list, and reference to references prop in `x-pack/tsconfig.json`. * Add reference to Uptime project in `x-pack/test/tsconfig.json`. * Add `tsconfig.json` project file to Uptime. * Fix broken JSON structure in test fixture. * Fix broken type exports. Introduce missing types. * Implement PR feedback. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/uptime/common/constants/alerts.ts | 40 ++++++++++++------- .../synthetics/waterfall/components/styles.ts | 16 ++++++-- .../actions_popover/actions_popover.tsx | 2 +- .../monitor_list_drawer/data.json | 20 +++++++++- .../uptime/public/state/alerts/alerts.ts | 2 +- .../public/state/certificates/certificates.ts | 2 +- x-pack/plugins/uptime/public/state/index.ts | 7 ++-- .../uptime/public/state/reducers/journey.ts | 2 +- .../server/lib/alerts/duration_anomaly.ts | 3 +- .../uptime/server/lib/alerts/status_check.ts | 3 +- .../plugins/uptime/server/lib/alerts/tls.ts | 3 +- x-pack/plugins/uptime/server/lib/lib.ts | 2 +- .../lib/requests/get_journey_details.ts | 2 +- .../lib/requests/get_journey_failed_steps.ts | 2 +- .../lib/requests/get_journey_screenshot.ts | 2 +- .../server/lib/requests/get_journey_steps.ts | 2 +- .../server/lib/requests/get_network_events.ts | 2 +- .../uptime/server/lib/requests/helper.ts | 15 +++++-- x-pack/plugins/uptime/tsconfig.json | 24 +++++++++++ x-pack/test/tsconfig.json | 6 ++- x-pack/tsconfig.json | 4 +- x-pack/tsconfig.refs.json | 3 +- 22 files changed, 117 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/uptime/tsconfig.json diff --git a/x-pack/plugins/uptime/common/constants/alerts.ts b/x-pack/plugins/uptime/common/constants/alerts.ts index a79a00c54c3de..46ce0b27e406b 100644 --- a/x-pack/plugins/uptime/common/constants/alerts.ts +++ b/x-pack/plugins/uptime/common/constants/alerts.ts @@ -7,23 +7,33 @@ import { ActionGroup } from '../../../alerts/common'; +export type MonitorStatusActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.monitorStatus'>; +export type TLSActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; +export type DurationAnomalyActionGroup = ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>; + +export const MONITOR_STATUS: MonitorStatusActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.monitorStatus', + name: 'Uptime Down Monitor', +}; + +export const TLS: TLSActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.tls', + name: 'Uptime TLS Alert', +}; + +export const DURATION_ANOMALY: DurationAnomalyActionGroup = { + id: 'xpack.uptime.alerts.actionGroups.durationAnomaly', + name: 'Uptime Duration Anomaly', +}; + export const ACTION_GROUP_DEFINITIONS: { - MONITOR_STATUS: ActionGroup<'xpack.uptime.alerts.actionGroups.monitorStatus'>; - TLS: ActionGroup<'xpack.uptime.alerts.actionGroups.tls'>; - DURATION_ANOMALY: ActionGroup<'xpack.uptime.alerts.actionGroups.durationAnomaly'>; + MONITOR_STATUS: MonitorStatusActionGroup; + TLS: TLSActionGroup; + DURATION_ANOMALY: DurationAnomalyActionGroup; } = { - MONITOR_STATUS: { - id: 'xpack.uptime.alerts.actionGroups.monitorStatus', - name: 'Uptime Down Monitor', - }, - TLS: { - id: 'xpack.uptime.alerts.actionGroups.tls', - name: 'Uptime TLS Alert', - }, - DURATION_ANOMALY: { - id: 'xpack.uptime.alerts.actionGroups.durationAnomaly', - name: 'Uptime Duration Anomaly', - }, + MONITOR_STATUS, + TLS, + DURATION_ANOMALY, }; export const CLIENT_ALERT_TYPES = { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index c00c04b114045..9177902f8a613 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiPanelProps } from '@elastic/eui'; import { rgba } from 'polished'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; +import { FunctionComponent } from 'react'; +import { StyledComponent } from 'styled-components'; +import { euiStyled, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; import { FIXED_AXIS_HEIGHT } from './constants'; interface WaterfallChartOuterContainerProps { @@ -53,7 +55,10 @@ export const WaterfallChartAxisOnlyContainer = euiStyled(EuiFlexItem)` export const WaterfallChartTopContainer = euiStyled(EuiFlexGroup)` `; -export const WaterfallChartFixedTopContainerSidebarCover = euiStyled(EuiPanel)` +export const WaterfallChartFixedTopContainerSidebarCover: StyledComponent< + FunctionComponent, + EuiTheme +> = euiStyled(EuiPanel)` height: 100%; border-radius: 0 !important; border: none; @@ -82,7 +87,10 @@ export const WaterfallChartSidebarContainer = euiStyled.div, + EuiTheme +> = euiStyled(EuiPanel)` border: 0; height: 100%; `; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx index ecc6231ba05fd..9ee6dc749b9eb 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/actions_popover/actions_popover.tsx @@ -12,7 +12,7 @@ import { IntegrationGroup } from './integration_group'; import { MonitorSummary } from '../../../../../../common/runtime_types'; import { toggleIntegrationsPopover, PopoverState } from '../../../../../state/actions'; -interface ActionsPopoverProps { +export interface ActionsPopoverProps { summary: MonitorSummary; popoverState: PopoverState | null; togglePopoverIsVisible: typeof toggleIntegrationsPopover; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json index 1bbdcd4a30078..905e982681dee 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/data.json @@ -261,7 +261,25 @@ }, "state": { "agent": null, - "checks": , + "checks": [ + { + "agent": { "id": "8f9a37fb-573a-4fdc-9895-440a5b39c250", "__typename": "Agent" }, + "container": null, + "kubernetes": null, + "monitor": { + "ip": "127.0.0.1", + "name": "localhost", + "status": "up", + "__typename": "CheckMonitor" + }, + "observer": { + "geo": { "name": null, "location": null, "__typename": "CheckGeo" }, + "__typename": "CheckObserver" + }, + "timestamp": "1570538246143", + "__typename": "Check" + } + ], "geo": null, "observer": { "geo": { "name": [], "location": null, "__typename": "StateGeo" }, diff --git a/x-pack/plugins/uptime/public/state/alerts/alerts.ts b/x-pack/plugins/uptime/public/state/alerts/alerts.ts index 4b48b157c3deb..f328bd5b9a5a7 100644 --- a/x-pack/plugins/uptime/public/state/alerts/alerts.ts +++ b/x-pack/plugins/uptime/public/state/alerts/alerts.ts @@ -53,7 +53,7 @@ export const deleteAnomalyAlertAction = createAsyncAction<{ alertId: string }, a 'DELETE ANOMALY ALERT' ); -interface AlertState { +export interface AlertState { connectors: AsyncInitState; newAlert: AsyncInitState>; alerts: AsyncInitState; diff --git a/x-pack/plugins/uptime/public/state/certificates/certificates.ts b/x-pack/plugins/uptime/public/state/certificates/certificates.ts index d6d48f2ab7007..ca2d5e7a17a46 100644 --- a/x-pack/plugins/uptime/public/state/certificates/certificates.ts +++ b/x-pack/plugins/uptime/public/state/certificates/certificates.ts @@ -19,7 +19,7 @@ export const getCertificatesAction = createAsyncAction; } diff --git a/x-pack/plugins/uptime/public/state/index.ts b/x-pack/plugins/uptime/public/state/index.ts index fa15e77f7fcc4..61b1a5f9d9527 100644 --- a/x-pack/plugins/uptime/public/state/index.ts +++ b/x-pack/plugins/uptime/public/state/index.ts @@ -5,17 +5,16 @@ * 2.0. */ -import { compose, createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension'; import createSagaMiddleware from 'redux-saga'; import { rootEffect } from './effects'; import { rootReducer } from './reducers'; export type AppState = ReturnType; -const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const sagaMW = createSagaMiddleware(); -export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(sagaMW))); +export const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMW))); sagaMW.run(rootEffect); diff --git a/x-pack/plugins/uptime/public/state/reducers/journey.ts b/x-pack/plugins/uptime/public/state/reducers/journey.ts index 273523f4592d6..361454e1b3fa1 100644 --- a/x-pack/plugins/uptime/public/state/reducers/journey.ts +++ b/x-pack/plugins/uptime/public/state/reducers/journey.ts @@ -24,7 +24,7 @@ export interface JourneyState { error?: Error; } -interface JourneyKVP { +export interface JourneyKVP { [checkGroup: string]: JourneyState; } diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 6310b79206a88..0c9f9dd849341 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -10,7 +10,7 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { ActionGroupIdsOf } from '../../../../alerts/common'; import { updateState } from './common'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { DURATION_ANOMALY } from '../../../common/constants/alerts'; import { commonStateTranslations, durationAnomalyTranslations } from './translations'; import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies'; import { getSeverityType } from '../../../../ml/common/util/anomaly_utils'; @@ -21,7 +21,6 @@ import { getMLJobId } from '../../../common/lib'; import { getLatestMonitor } from '../requests/get_latest_monitor'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; -const { DURATION_ANOMALY } = ACTION_GROUP_DEFINITIONS; export type ActionGroupIds = ActionGroupIdsOf; export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Ping) => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index cc1cb3a4ed0be..cee20d113c256 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -17,7 +17,7 @@ import { Ping, GetMonitorAvailabilityParams, } from '../../../common/runtime_types'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { MONITOR_STATUS } from '../../../common/constants/alerts'; import { updateState } from './common'; import { commonMonitorStateI18, commonStateTranslations, DOWN_LABEL } from './translations'; import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib'; @@ -29,7 +29,6 @@ import { MonitorStatusTranslations } from '../../../common/translations'; import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern'; import { UMServerLibs, UptimeESClient } from '../lib'; -const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; export type ActionGroupIds = ActionGroupIdsOf; const getMonIdByLoc = (monitorId: string, location: string) => { diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 345d2470ed705..7bc4c36b98e8b 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { UptimeAlertTypeFactory } from './types'; import { updateState } from './common'; -import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { TLS } from '../../../common/constants/alerts'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { Cert, CertResult } from '../../../common/runtime_types'; import { commonStateTranslations, tlsTranslations } from './translations'; @@ -17,7 +17,6 @@ import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; import { ActionGroupIdsOf } from '../../../../alerts/common'; -const { TLS } = ACTION_GROUP_DEFINITIONS; export type ActionGroupIds = ActionGroupIdsOf; const DEFAULT_SIZE = 20; diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index 53a79815a0c0f..5ac56d14c171d 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -27,7 +27,7 @@ export interface UMServerLibs extends UMDomainLibs { framework: UMBackendFrameworkAdapter; } -interface CountResponse { +export interface CountResponse { body: { count: number; _shards: { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts index c942c3a8f69fd..e0edcc4576378 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_details.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { SyntheticsJourneyApiResponse } from '../../../common/runtime_types'; -interface GetJourneyDetails { +export interface GetJourneyDetails { checkGroup: string; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts index 1abba0087cb44..9865bd95fe961 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_failed_steps.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; -interface GetJourneyStepsParams { +export interface GetJourneyStepsParams { checkGroups: string[]; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts index ff9aec85e28bb..9cb5e1eedb6b0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_screenshot.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types/ping'; -interface GetJourneyScreenshotParams { +export interface GetJourneyScreenshotParams { checkGroup: string; stepIndex: number; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts index d657b8b9aacf3..3055f169fc495 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_journey_steps.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { Ping } from '../../../common/runtime_types'; -interface GetJourneyStepsParams { +export interface GetJourneyStepsParams { checkGroup: string; syntheticEventTypes?: string | string[]; } diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index f9936c6f273ba..fa76da0025305 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -8,7 +8,7 @@ import { UMElasticsearchQueryFn } from '../adapters/framework'; import { NetworkEvent } from '../../../common/runtime_types'; -interface GetNetworkEventsParams { +export interface GetNetworkEventsParams { checkGroup: string; stepIndex: string; } diff --git a/x-pack/plugins/uptime/server/lib/requests/helper.ts b/x-pack/plugins/uptime/server/lib/requests/helper.ts index 2556d7b8fb8cd..e3969f84c8485 100644 --- a/x-pack/plugins/uptime/server/lib/requests/helper.ts +++ b/x-pack/plugins/uptime/server/lib/requests/helper.ts @@ -5,14 +5,14 @@ * 2.0. */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ElasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; import { elasticsearchServiceMock, savedObjectsClientMock, } from '../../../../../../src/core/server/mocks'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ElasticsearchClientMock } from '../../../../../../src/core/server/elasticsearch/client/mocks'; -import { createUptimeESClient } from '../lib'; +import { createUptimeESClient, UptimeESClient } from '../lib'; export interface MultiPageCriteria { after_key?: K; @@ -60,7 +60,14 @@ export const setupMockEsCompositeQuery = ( return esMock; }; -export const getUptimeESMockClient = (esClientMock?: ElasticsearchClientMock) => { +interface UptimeEsMockClient { + esClient: ElasticsearchClientMock; + uptimeEsClient: UptimeESClient; +} + +export const getUptimeESMockClient = ( + esClientMock?: ElasticsearchClientMock +): UptimeEsMockClient => { const esClient = elasticsearchServiceMock.createElasticsearchClient(); const savedObjectsClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/uptime/tsconfig.json b/x-pack/plugins/uptime/tsconfig.json new file mode 100644 index 0000000000000..5a195f6c2df25 --- /dev/null +++ b/x-pack/plugins/uptime/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "public/components/monitor/status_details/location_map/embeddables/low_poly_layer.json", + "server/**/*", + "server/lib/requests/__fixtures__/monitor_charts_mock.json", + "../../../typings/**/*" + ], + "references": [ + { "path": "../alerts/tsconfig.json" }, + { "path": "../ml/tsconfig.json" }, + { "path": "../triggers_actions_ui/tsconfig.json" }, + { "path": "../observability/tsconfig.json" } + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 0a7a30f373e07..2981346e80e1d 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -76,6 +76,10 @@ { "path": "../plugins/triggers_actions_ui/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, { "path": "../plugins/upgrade_assistant/tsconfig.json" }, - { "path": "../plugins/watcher/tsconfig.json" } + { "path": "../plugins/watcher/tsconfig.json" }, + { "path": "../plugins/runtime_fields/tsconfig.json" }, + { "path": "../plugins/index_management/tsconfig.json" }, + { "path": "../plugins/watcher/tsconfig.json" }, + { "path": "../plugins/uptime/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 5d51c2923abd0..740bac3f1b0de 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -56,6 +56,7 @@ "plugins/index_management/**/*", "plugins/grokdebugger/**/*", "plugins/upgrade_assistant/**/*", + "plugins/uptime/**/*", "test/**/*" ], "compilerOptions": { @@ -145,6 +146,7 @@ { "path": "./plugins/upgrade_assistant/tsconfig.json" }, { "path": "./plugins/runtime_fields/tsconfig.json" }, { "path": "./plugins/index_management/tsconfig.json" }, - { "path": "./plugins/watcher/tsconfig.json" } + { "path": "./plugins/watcher/tsconfig.json" }, + { "path": "./plugins/uptime/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index ae88ab6486e64..7a2eebc78b69b 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -50,6 +50,7 @@ { "path": "./plugins/upgrade_assistant/tsconfig.json" }, { "path": "./plugins/runtime_fields/tsconfig.json" }, { "path": "./plugins/index_management/tsconfig.json" }, - { "path": "./plugins/watcher/tsconfig.json" } + { "path": "./plugins/watcher/tsconfig.json" }, + { "path": "./plugins/uptime/tsconfig.json" } ] } From a997178c0e8982322bffe1e79b2eab6ce1d39e77 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Tue, 9 Feb 2021 08:51:49 +0200 Subject: [PATCH 17/19] Fix vega renovate label (#90591) --- renovate.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renovate.json5 b/renovate.json5 index 1585627daa880..f1e773427a103 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -57,7 +57,7 @@ groupName: 'vega related modules', packageNames: ['vega', 'vega-lite', 'vega-schema-url-parser', 'vega-tooltip'], reviewers: ['team:kibana-app'], - labels: ['Feature:Lens', 'Team:KibanaApp'], + labels: ['Feature:Vega', 'Team:KibanaApp'], enabled: true, }, ], From 451d0819bcc12c54415ca21afed50abac01d46c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 9 Feb 2021 08:26:36 +0100 Subject: [PATCH 18/19] Strongly typed EUI theme for styled-components (#90106) * Strongly typed EUI theme for styled-components use euiStyled fix tsc issue * use relative imports * remove redundant types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/apm/public/application/csmApp.tsx | 5 +++-- x-pack/plugins/apm/public/application/index.tsx | 5 +++-- .../app/ErrorGroupDetails/DetailView/index.tsx | 6 +++--- .../components/app/ErrorGroupDetails/index.tsx | 10 +++++----- .../LocalUIFilters/Filter/FilterBadgeList.tsx | 4 ++-- .../LocalUIFilters/Filter/FilterTitleButton.tsx | 4 ++-- .../RumDashboard/LocalUIFilters/Filter/index.tsx | 12 ++++++------ .../app/RumDashboard/LocalUIFilters/index.tsx | 4 ++-- .../components/app/ServiceMap/Controls.tsx | 10 +++++----- .../components/app/ServiceMap/EmptyBanner.tsx | 4 ++-- .../app/ServiceMap/Popover/AnomalyDetection.tsx | 12 ++++++------ .../components/app/ServiceMap/Popover/Info.tsx | 8 ++++---- .../app/ServiceMap/Popover/ServiceStatsList.tsx | 8 ++++---- .../public/components/app/ServiceMap/index.tsx | 4 ++-- .../public/components/app/TraceLink/index.tsx | 4 ++-- .../app/error_group_overview/List/index.tsx | 12 ++++++------ .../app/service_inventory/ServiceList/index.tsx | 6 +++--- .../app/service_node_metrics/index.tsx | 6 +++--- .../app/service_node_overview/index.tsx | 4 ++-- .../service_overview_table_container.tsx | 4 ++-- .../components/app/trace_overview/TraceList.tsx | 4 ++-- .../WaterfallContainer/ServiceLegends.tsx | 4 ++-- .../Waterfall/ResponsiveFlyout.tsx | 5 ++--- .../Waterfall/SpanFlyout/DatabaseContext.tsx | 4 ++-- .../Waterfall/SpanFlyout/HttpContext.tsx | 5 ++--- .../SpanFlyout/TruncateHeightSection.tsx | 4 ++-- .../Waterfall/SpanFlyout/index.tsx | 8 ++++---- .../WaterfallContainer/Waterfall/SyncBadge.tsx | 6 +++--- .../Waterfall/WaterfallItem.tsx | 8 ++++---- .../Waterfall/accordion_waterfall.tsx | 6 +++--- .../WaterfallContainer/Waterfall/index.tsx | 6 +++--- .../TransactionList/index.tsx | 4 ++-- .../public/components/shared/ApmHeader/index.tsx | 4 ++-- .../shared/KeyValueTable/FormattedValue.tsx | 4 ++-- .../shared/KueryBar/Typeahead/Suggestion.js | 10 +++++----- .../shared/KueryBar/Typeahead/Suggestions.js | 4 ++-- .../public/components/shared/KueryBar/index.tsx | 4 ++-- .../shared/Stacktrace/CauseStacktrace.tsx | 10 +++++----- .../components/shared/Stacktrace/Context.tsx | 16 ++++++++-------- .../shared/Stacktrace/FrameHeading.tsx | 8 ++++---- .../shared/Stacktrace/LibraryStacktrace.tsx | 4 ++-- .../components/shared/Stacktrace/Stackframe.tsx | 6 +++--- .../components/shared/Stacktrace/Variables.tsx | 4 ++-- .../components/shared/StickyProperties/index.tsx | 10 +++++----- .../Summary/ErrorCountSummaryItemBadge.tsx | 6 +++--- .../shared/Summary/HttpInfoSummaryItem/index.tsx | 10 +++++----- .../shared/Summary/UserAgentSummaryItem.tsx | 4 ++-- .../public/components/shared/Summary/index.tsx | 4 ++-- .../components/shared/charts/Legend/index.tsx | 6 +++--- .../charts/Timeline/Marker/AgentMarker.tsx | 6 +++--- .../charts/Timeline/Marker/ErrorMarker.tsx | 10 +++++----- .../shared/charts/Timeline/Marker/index.tsx | 4 ++-- .../charts/transaction_charts/ml_header.tsx | 6 +++--- .../apm/public/components/shared/main_tabs.tsx | 4 ++-- .../apm/public/components/shared/search_bar.tsx | 4 ++-- .../components/shared/time_comparison/index.tsx | 4 ++-- .../shared/truncate_with_tooltip/index.tsx | 6 +++--- 57 files changed, 177 insertions(+), 177 deletions(-) diff --git a/x-pack/plugins/apm/public/application/csmApp.tsx b/x-pack/plugins/apm/public/application/csmApp.tsx index 5fdd45336eb72..8ea4593bb89a7 100644 --- a/x-pack/plugins/apm/public/application/csmApp.tsx +++ b/x-pack/plugins/apm/public/application/csmApp.tsx @@ -11,7 +11,8 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router } from 'react-router-dom'; -import styled, { DefaultTheme, ThemeProvider } from 'styled-components'; +import { DefaultTheme, ThemeProvider } from 'styled-components'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider, RedirectAppLinks, @@ -30,7 +31,7 @@ import { createCallApmApi } from '../services/rest/createCallApmApi'; import { px, units } from '../style/variables'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; -const CsmMainContainer = styled.div` +const CsmMainContainer = euiStyled.div` padding: ${px(units.plus)}; height: 100%; `; diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index 1996cf3bfe2d9..0028b392fc838 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -12,7 +12,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import 'react-vis/dist/style.css'; -import styled, { DefaultTheme, ThemeProvider } from 'styled-components'; +import { DefaultTheme, ThemeProvider } from 'styled-components'; +import { euiStyled } from '../../../../../src/plugins/kibana_react/common'; import { ConfigSchema } from '../'; import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { @@ -35,7 +36,7 @@ import { createStaticIndexPattern } from '../services/rest/index_pattern'; import { setHelpExtension } from '../setHelpExtension'; import { setReadonlyBadge } from '../updateBadge'; -const MainContainer = styled.div` +const MainContainer = euiStyled.div` height: 100%; `; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index ebd15262fd089..cd893c1736988 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -20,7 +20,7 @@ import { Location } from 'history'; import { first } from 'lodash'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; import type { IUrlParams } from '../../../../context/url_params_context/types'; @@ -42,14 +42,14 @@ import { } from './ErrorTabs'; import { ExceptionStacktrace } from './ExceptionStacktrace'; -const HeaderContainer = styled.div` +const HeaderContainer = euiStyled.div` display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: ${px(unit)}; `; -const TransactionLinkName = styled.div` +const TransactionLinkName = euiStyled.div` margin-left: ${px(units.half)}; display: inline-block; vertical-align: middle; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index dfc5986f88228..9a8c2dffacaf7 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -19,7 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTrackPageview } from '../../../../../observability/public'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; import { useFetcher } from '../../../hooks/use_fetcher'; @@ -31,24 +31,24 @@ import { DetailView } from './DetailView'; import { ErrorDistribution } from './Distribution'; import { useErrorGroupDistributionFetcher } from '../../../hooks/use_error_group_distribution_fetcher'; -const Titles = styled.div` +const Titles = euiStyled.div` margin-bottom: ${px(units.plus)}; `; -const Label = styled.div` +const Label = euiStyled.div` margin-bottom: ${px(units.quarter)}; font-size: ${fontSizes.small}; color: ${({ theme }) => theme.eui.euiColorMediumShade}; `; -const Message = styled.div` +const Message = euiStyled.div` font-family: ${fontFamilyCode}; font-weight: bold; font-size: ${fontSizes.large}; margin-bottom: ${px(units.half)}; `; -const Culprit = styled.div` +const Culprit = euiStyled.div` font-family: ${fontFamilyCode}; `; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx index 6bc345ea5bd87..785d50de64553 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterBadgeList.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui'; -import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { unit, px, truncate } from '../../../../../style/variables'; -const BadgeText = styled.div` +const BadgeText = euiStyled.div` display: inline-block; ${truncate(px(unit * 8))}; vertical-align: middle; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx index 5d60f7c2aa332..1a59b7d910b1f 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/FilterTitleButton.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { EuiButtonEmpty, EuiTitle } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -const Button = styled(EuiButtonEmpty).attrs(() => ({ +const Button = euiStyled(EuiButtonEmpty).attrs(() => ({ contentProps: { className: 'alignLeft', }, diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx index e1debde1117f9..391766a0cf927 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/Filter/index.tsx @@ -19,12 +19,12 @@ import { EuiFlexGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { FilterBadgeList } from './FilterBadgeList'; import { unit, px } from '../../../../../style/variables'; import { FilterTitleButton } from './FilterTitleButton'; -const Popover = styled((EuiPopover as unknown) as FunctionComponent).attrs( +const Popover = euiStyled((EuiPopover as unknown) as FunctionComponent).attrs( () => ({ anchorClassName: 'anchor', }) @@ -34,22 +34,22 @@ const Popover = styled((EuiPopover as unknown) as FunctionComponent).attrs( } `; -const SelectContainer = styled.div` +const SelectContainer = euiStyled.div` width: ${px(unit * 16)}; `; -const Counter = styled.div` +const Counter = euiStyled.div` border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; background: ${({ theme }) => theme.eui.euiColorLightShade}; padding: 0 ${({ theme }) => theme.eui.paddingSizes.xs}; `; -const ApplyButton = styled(EuiButton)` +const ApplyButton = euiStyled(EuiButton)` align-self: flex-end; `; // needed for IE11 -const FlexItem = styled(EuiFlexItem)` +const FlexItem = euiStyled(EuiFlexItem)` flex-basis: auto !important; `; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx index a07997fb74921..4afecb7623f73 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx @@ -13,7 +13,7 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../hooks/useLocalUIFilters'; import { LocalUIFilterName } from '../../../../../common/ui_filter'; @@ -26,7 +26,7 @@ interface Props { shouldFetch?: boolean; } -const ButtonWrapper = styled.div` +const ButtonWrapper = euiStyled.div` display: inline-block; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx index 9737f6a5e2eba..3362219fd5f2d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -8,7 +8,7 @@ import { EuiButtonIcon, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext, useEffect, useState } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useTheme } from '../../../hooks/use_theme'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -17,23 +17,23 @@ import { APMQueryParams } from '../../shared/Links/url_helpers'; import { CytoscapeContext } from './Cytoscape'; import { getAnimationOptions, getNodeHeight } from './cytoscape_options'; -const ControlsContainer = styled('div')` +const ControlsContainer = euiStyled('div')` left: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; position: absolute; top: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ `; -const Button = styled(EuiButtonIcon)` +const Button = euiStyled(EuiButtonIcon)` display: block; margin: ${({ theme }) => theme.eui.paddingSizes.xs}; `; -const ZoomInButton = styled(Button)` +const ZoomInButton = euiStyled(Button)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.s}; `; -const Panel = styled(EuiPanel)` +const Panel = euiStyled(EuiPanel)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.s}; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx index 0cbf3f013f148..90caa9c87c484 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/EmptyBanner.tsx @@ -8,12 +8,12 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { CytoscapeContext } from './Cytoscape'; import { useTheme } from '../../../hooks/use_theme'; -const EmptyBannerContainer = styled.div` +const EmptyBannerContainer = euiStyled.div` margin: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; /* Add some extra margin so it displays to the right of the controls. */ left: calc( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index 50b1502a86fd3..c98116a69da66 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, @@ -15,6 +14,7 @@ import { EuiIconTip, EuiHealth, } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { getServiceHealthStatus, getServiceHealthStatusColor, @@ -30,28 +30,28 @@ import { ServiceAnomalyStats, } from '../../../../../common/anomaly_detection'; -const HealthStatusTitle = styled(EuiTitle)` +const HealthStatusTitle = euiStyled(EuiTitle)` display: inline; text-transform: uppercase; `; -const VerticallyCentered = styled.div` +const VerticallyCentered = euiStyled.div` display: flex; align-items: center; `; -const SubduedText = styled.span` +const SubduedText = euiStyled.span` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; `; -const EnableText = styled.section` +const EnableText = euiStyled.section` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; line-height: 1.4; font-size: ${fontSize}; width: ${px(popoverWidth)}; `; -export const ContentLine = styled.section` +export const ContentLine = euiStyled.section` line-height: 2; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 4900d1dedbde5..9577a02d68cf2 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -13,24 +13,24 @@ import { import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; import React, { Fragment } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { SPAN_SUBTYPE, SPAN_TYPE, } from '../../../../../common/elasticsearch_fieldnames'; import { ExternalConnectionNode } from '../../../../../common/service_map'; -const ItemRow = styled.div` +const ItemRow = euiStyled.div` line-height: 2; `; -const SubduedDescriptionListTitle = styled(EuiDescriptionListTitle)` +const SubduedDescriptionListTitle = euiStyled(EuiDescriptionListTitle)` &&& { color: ${({ theme }) => theme.eui.euiTextSubduedColor}; } `; -const ExternalResourcesList = styled.section` +const ExternalResourcesList = euiStyled.section` max-height: 360px; overflow: auto; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx index 65508e6adc0ca..766debc6d5587 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { asDuration, asPercent, @@ -16,16 +16,16 @@ import { } from '../../../../../common/utils/formatters'; import { ServiceNodeStats } from '../../../../../common/service_map'; -export const ItemRow = styled('tr')` +export const ItemRow = euiStyled('tr')` line-height: 2; `; -export const ItemTitle = styled('td')` +export const ItemTitle = euiStyled('td')` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; padding-right: 1rem; `; -export const ItemDescription = styled('td')` +export const ItemDescription = euiStyled('td')` text-align: right; `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 7021575da905e..7ef3cbca3ad2f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import React, { PropsWithChildren, ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { useTrackPageview } from '../../../../../observability/public'; import { @@ -33,7 +33,7 @@ interface ServiceMapProps { serviceName?: string; } -const ServiceMapDatePickerFlexGroup = styled(EuiFlexGroup)` +const ServiceMapDatePickerFlexGroup = euiStyled(EuiFlexGroup)` padding: ${({ theme }) => theme.eui.euiSizeM}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; margin: 0; diff --git a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx index 302b815f78715..d0c2b5c598039 100644 --- a/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceLink/index.tsx @@ -8,13 +8,13 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import React from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { getRedirectToTransactionDetailPageUrl } from './get_redirect_to_transaction_detail_page_url'; import { getRedirectToTracePageUrl } from './get_redirect_to_trace_page_url'; -const CentralizedContainer = styled.div` +const CentralizedContainer = euiStyled.div` height: 100%; display: flex; `; diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx index 9d891151e75d2..66fb72975acea 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/index.tsx @@ -9,8 +9,8 @@ import { EuiBadge, EuiToolTip } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; import { EuiIconTip } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; import { @@ -27,25 +27,25 @@ import { TimestampTooltip } from '../../../shared/TimestampTooltip'; import { ErrorOverviewLink } from '../../../shared/Links/apm/ErrorOverviewLink'; import { APMQueryParams } from '../../../shared/Links/url_helpers'; -const GroupIdLink = styled(ErrorDetailLink)` +const GroupIdLink = euiStyled(ErrorDetailLink)` font-family: ${fontFamilyCode}; `; -const MessageAndCulpritCell = styled.div` +const MessageAndCulpritCell = euiStyled.div` ${truncate('100%')}; `; -const ErrorLink = styled(ErrorOverviewLink)` +const ErrorLink = euiStyled(ErrorOverviewLink)` ${truncate('100%')}; `; -const MessageLink = styled(ErrorDetailLink)` +const MessageLink = euiStyled(ErrorDetailLink)` font-family: ${fontFamilyCode}; font-size: ${fontSizes.large}; ${truncate('100%')}; `; -const Culprit = styled.div` +const Culprit = euiStyled.div` font-family: ${fontFamilyCode}; `; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx index 4506700380390..5287e6699aaee 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx @@ -8,11 +8,11 @@ import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; import { EuiIcon } from '@elastic/eui'; import { EuiText } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, @@ -46,12 +46,12 @@ function formatString(value?: string | null) { return value || NOT_AVAILABLE_LABEL; } -const AppLink = styled(ServiceOrTransactionsOverviewLink)` +const AppLink = euiStyled(ServiceOrTransactionsOverviewLink)` font-size: ${fontSizes.large}; ${truncate('100%')}; `; -const ToolTipWrapper = styled.span` +const ToolTipWrapper = euiStyled.span` width: 100%; .apmServiceList__serviceNameTooltip { width: 100%; diff --git a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx index 5832f2b7d1ac9..21871a17f4b04 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_metrics/index.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; @@ -39,12 +39,12 @@ const INITIAL_DATA = { containerId: '', }; -const Truncate = styled.span` +const Truncate = euiStyled.span` display: block; ${truncate(px(unit * 12))} `; -const MetadataFlexGroup = styled(EuiFlexGroup)` +const MetadataFlexGroup = euiStyled(EuiFlexGroup)` border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; margin-bottom: ${({ theme }) => theme.eui.paddingSizes.m}; padding: ${({ theme }) => diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 00d184f692e3b..c64bbcb569dde 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiPage, EuiPanel, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { @@ -26,7 +26,7 @@ const INITIAL_PAGE_SIZE = 25; const INITIAL_SORT_FIELD = 'cpu'; const INITIAL_SORT_DIRECTION = 'desc'; -const ServiceNodeName = styled.div` +const ServiceNodeName = euiStyled.div` ${truncate(px(8 * unit))} `; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx index 45d34cd304ce7..738ff0d7c735f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_table_container.tsx @@ -6,7 +6,7 @@ */ import React, { ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useBreakPoints } from '../../../hooks/use_break_points'; /** @@ -24,7 +24,7 @@ const tableHeight = 282; * * Hide the empty message when we don't yet have any items and are still loading. */ -const ServiceOverviewTableContainerDiv = styled.div<{ +const ServiceOverviewTableContainerDiv = euiStyled.div<{ isEmptyAndLoading: boolean; shouldUseMobileLayout: boolean; }>` diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx index cdb82418180ba..774333c35b479 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/TraceList.tsx @@ -8,7 +8,7 @@ import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { asMillisecondDuration, asTransactionRate, @@ -23,7 +23,7 @@ import { APIReturnType } from '../../../services/rest/createCallApmApi'; type TraceGroup = APIReturnType<'GET /api/apm/traces'>['items'][0]; -const StyledTransactionLink = styled(TransactionDetailLink)` +const StyledTransactionLink = euiStyled(TransactionDetailLink)` font-size: ${fontSizes.large}; ${truncate('100%')}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx index 2f4c3e3a9d24c..ab3773b2cac2e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/ServiceLegends.tsx @@ -8,12 +8,12 @@ import { EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { px, unit } from '../../../../../style/variables'; import { Legend } from '../../../../shared/charts/Legend'; import { IServiceColors } from './Waterfall/waterfall_helpers/waterfall_helpers'; -const Legends = styled.div` +const Legends = euiStyled.div` display: flex; > * { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx index 2812c686d7121..8549f09bba248 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx @@ -6,10 +6,9 @@ */ import { EuiFlyout } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; -import styled from 'styled-components'; - -export const ResponsiveFlyout = styled(EuiFlyout)` +export const ResponsiveFlyout = euiStyled(EuiFlyout)` width: 100%; @media (min-width: 800px) { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx index 3509500d9f429..fda2d595e669d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/DatabaseContext.tsx @@ -12,7 +12,7 @@ import React, { Fragment } from 'react'; import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; import { borderRadius, @@ -26,7 +26,7 @@ import { TruncateHeightSection } from './TruncateHeightSection'; SyntaxHighlighter.registerLanguage('sql', sql); -const DatabaseStatement = styled.div` +const DatabaseStatement = euiStyled.div` padding: ${px(units.half)} ${px(unit)}; background: ${({ theme }) => tint(0.1, theme.eui.euiColorWarning)}; border-radius: ${borderRadius}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx index 065dadc6dfd0d..3584309ebb20c 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/HttpContext.tsx @@ -6,9 +6,8 @@ */ import React, { Fragment } from 'react'; -import styled from 'styled-components'; - import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { borderRadius, fontFamilyCode, @@ -19,7 +18,7 @@ import { } from '../../../../../../../style/variables'; import { Span } from '../../../../../../../../typings/es_schemas/ui/span'; -const ContextUrl = styled.div` +const ContextUrl = euiStyled.div` padding: ${px(units.half)} ${px(unit)}; background: ${({ theme }) => theme.eui.euiColorLightestShade}; border-radius: ${borderRadius}; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx index 401c34ed32436..181fcb91ba3e6 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/TruncateHeightSection.tsx @@ -8,10 +8,10 @@ import { EuiIcon, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment, ReactNode, useEffect, useRef, useState } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../../../../style/variables'; -const ToggleButtonContainer = styled.div` +const ToggleButtonContainer = euiStyled.div` margin-top: ${px(units.half)}; user-select: none; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index 35f71676da20e..fe4384e84427f 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../../../../style/variables'; import { Summary } from '../../../../../../shared/Summary'; import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; @@ -72,12 +72,12 @@ function getSpanTypes(span: Span) { }; } -const SpanBadge = (styled(EuiBadge)` +const SpanBadge = euiStyled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -` as unknown) as typeof EuiBadge; +`; -const HttpInfoContainer = styled('div')` +const HttpInfoContainer = euiStyled('div')` margin-right: ${px(units.quarter)}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx index cfc90741b0469..24301b2cf10fb 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/SyncBadge.tsx @@ -8,13 +8,13 @@ import { EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../../../style/variables'; -const SpanBadge = (styled(EuiBadge)` +const SpanBadge = euiStyled(EuiBadge)` display: inline-block; margin-right: ${px(units.quarter)}; -` as unknown) as typeof EuiBadge; +`; interface SyncBadgeProps { /** diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index eb34b457d756d..7000f389e3d0e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -6,10 +6,10 @@ */ import React, { ReactNode } from 'react'; -import styled from 'styled-components'; import { EuiIcon, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../../common/utils/formatters'; import { isRumAgentName } from '../../../../../../../common/agent_name'; import { px, unit, units } from '../../../../../../style/variables'; @@ -33,7 +33,7 @@ interface IBarStyleProps { color: string; } -const Container = styled.div` +const Container = euiStyled.div` position: relative; display: block; user-select: none; @@ -50,7 +50,7 @@ const Container = styled.div` } `; -const ItemBar = styled.div` +const ItemBar = euiStyled.div` box-sizing: border-box; position: relative; height: ${px(unit)}; @@ -58,7 +58,7 @@ const ItemBar = styled.div` background-color: ${(props) => props.color}; `; -const ItemText = styled.span` +const ItemText = euiStyled.span` position: absolute; right: 0; display: flex; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx index 08bd8c21b7649..8d50074d814eb 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/accordion_waterfall.tsx @@ -9,7 +9,7 @@ import { EuiAccordion, EuiAccordionProps } from '@elastic/eui'; import { Location } from 'history'; import { isEmpty } from 'lodash'; import React, { useState } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { Margins } from '../../../../../shared/charts/Timeline'; import { WaterfallItem } from './WaterfallItem'; import { @@ -32,7 +32,7 @@ interface AccordionWaterfallProps { onClickWaterfallItem: (item: IWaterfallItem) => void; } -const StyledAccordion = styled(EuiAccordion).withConfig({ +const StyledAccordion = euiStyled(EuiAccordion).withConfig({ shouldForwardProp: (prop) => !['childrenCount', 'marginLeftLevel', 'hasError'].includes(prop), })< @@ -86,7 +86,7 @@ const StyledAccordion = styled(EuiAccordion).withConfig({ }} `; -const WaterfallItemContainer = styled.div` +const WaterfallItemContainer = euiStyled.div` position: absolute; width: 100%; left: 0; diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index 2ee3b53242a78..a680fdc404402 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { History, Location } from 'history'; import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; import { Timeline } from '../../../../../shared/charts/Timeline'; import { HeightRetainer } from '../../../../../shared/HeightRetainer'; import { fromQuery, toQuery } from '../../../../../shared/Links/url_helpers'; @@ -23,7 +23,7 @@ import { IWaterfallItem, } from './waterfall_helpers/waterfall_helpers'; -const Container = styled.div` +const Container = euiStyled.div` transition: 0.1s padding ease; position: relative; overflow: hidden; @@ -55,7 +55,7 @@ const toggleFlyout = ({ }); }; -const WaterfallItemsContainer = styled.div` +const WaterfallItemsContainer = euiStyled.div` border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; `; diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx index 9a1a691154b18..795a6e66f70a4 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx @@ -8,7 +8,7 @@ import { EuiToolTip, EuiIconTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { @@ -26,7 +26,7 @@ type TransactionGroup = APIReturnType<'GET /api/apm/services/{serviceName}/trans // Truncate both the link and the child span (the tooltip anchor.) The link so // it doesn't overflow, and the anchor so we get the ellipsis. -const TransactionNameLink = styled(TransactionDetailLink)` +const TransactionNameLink = euiStyled(TransactionDetailLink)` font-family: ${fontFamilyCode}; white-space: nowrap; ${truncate('100%')}; diff --git a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx index f03c9dd0a2332..414011df7f9ef 100644 --- a/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ApmHeader/index.tsx @@ -7,13 +7,13 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { HeaderMenuPortal } from '../../../../../observability/public'; import { ActionMenu } from '../../../application/action_menu'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { EnvironmentFilter } from '../EnvironmentFilter'; -const HeaderFlexGroup = styled(EuiFlexGroup)` +const HeaderFlexGroup = euiStyled(EuiFlexGroup)` padding: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; `; diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx index 6ab4f2e0388b4..ed91aefdfcf9e 100644 --- a/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx +++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/FormattedValue.tsx @@ -7,10 +7,10 @@ import { isBoolean, isNumber, isObject } from 'lodash'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; -const EmptyValue = styled.span` +const EmptyValue = euiStyled.span` color: ${({ theme }) => theme.eui.euiColorMediumShade}; text-align: left; `; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js index fe767f86239b1..46da6fe4be4c9 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestion.js @@ -7,7 +7,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { EuiIcon } from '@elastic/eui'; import { fontFamilyCode, @@ -33,7 +33,7 @@ function getIconColor(type, theme) { } } -const Description = styled.div` +const Description = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorDarkShade}; p { @@ -48,7 +48,7 @@ const Description = styled.div` } `; -const ListItem = styled.li` +const ListItem = euiStyled.li` font-size: ${fontSizes.small}; height: ${px(units.double)}; align-items: center; @@ -68,7 +68,7 @@ const ListItem = styled.li` } `; -const Icon = styled.div` +const Icon = euiStyled.div` flex: 0 0 ${px(units.double)}; background: ${({ type, theme }) => tint(0.1, getIconColor(type, theme))}; color: ${({ type, theme }) => getIconColor(type, theme)}; @@ -78,7 +78,7 @@ const Icon = styled.div` line-height: ${px(units.double)}; `; -const TextValue = styled.div` +const TextValue = euiStyled.div` flex: 0 0 ${px(unit * 16)}; color: ${({ theme }) => theme.eui.euiColorDarkestShade}; padding: 0 ${px(units.half)}; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js index ce0fcab5dea1c..cbbf762fa341c 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/Typeahead/Suggestions.js @@ -7,13 +7,13 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { isEmpty } from 'lodash'; import Suggestion from './Suggestion'; import { units, px, unit } from '../../../../style/variables'; import { tint } from 'polished'; -const List = styled.ul` +const List = euiStyled.ul` width: 100%; border: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; border-radius: ${px(units.quarter)}; diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx index 98eb0548b8521..efa4f26d9a23f 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { startsWith, uniqueId } from 'lodash'; import React, { useState } from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { esKuery, IIndexPattern, @@ -24,7 +24,7 @@ import { getBoolFilter } from './get_bool_filter'; import { Typeahead } from './Typeahead'; import { useProcessorEvent } from './use_processor_event'; -const Container = styled.div` +const Container = euiStyled.div` margin-bottom: 10px; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx index 7f8c68ee32ef8..090ba0e8e28cf 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/CauseStacktrace.tsx @@ -6,23 +6,23 @@ */ import React from 'react'; -import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiAccordion, EuiTitle } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { px, unit, units } from '../../../style/variables'; import { Stacktrace } from '.'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; -const Accordion = styled(EuiAccordion)` +const Accordion = euiStyled(EuiAccordion)` border-top: ${({ theme }) => theme.eui.euiBorderThin}; margin-top: ${px(units.half)}; `; -const CausedByContainer = styled('h5')` +const CausedByContainer = euiStyled('h5')` padding: ${({ theme }) => theme.eui.spacerSizes.s} 0; `; -const CausedByHeading = styled('span')` +const CausedByHeading = euiStyled('span')` color: ${({ theme }) => theme.eui.euiTextSubduedColor}; display: block; font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; @@ -30,7 +30,7 @@ const CausedByHeading = styled('span')` text-transform: uppercase; `; -const FramesContainer = styled('div')` +const FramesContainer = euiStyled('div')` padding-left: ${px(unit)}; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx index 7a503258b2e58..85d29dda95b5c 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Context.tsx @@ -13,7 +13,7 @@ import python from 'react-syntax-highlighter/dist/cjs/languages/hljs/python'; import ruby from 'react-syntax-highlighter/dist/cjs/languages/hljs/ruby'; import xcode from 'react-syntax-highlighter/dist/cjs/styles/hljs/xcode'; import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { StackframeWithLineContext } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { borderRadius, px, unit, units } from '../../../style/variables'; @@ -21,13 +21,13 @@ SyntaxHighlighter.registerLanguage('javascript', javascript); SyntaxHighlighter.registerLanguage('python', python); SyntaxHighlighter.registerLanguage('ruby', ruby); -const ContextContainer = styled.div` +const ContextContainer = euiStyled.div` position: relative; border-radius: ${borderRadius}; `; const LINE_HEIGHT = units.eighth * 9; -const LineHighlight = styled.div<{ lineNumber: number }>` +const LineHighlight = euiStyled.div<{ lineNumber: number }>` position: absolute; width: 100%; height: ${px(units.eighth * 9)}; @@ -36,7 +36,7 @@ const LineHighlight = styled.div<{ lineNumber: number }>` background-color: ${({ theme }) => tint(0.1, theme.eui.euiColorWarning)}; `; -const LineNumberContainer = styled.div<{ isLibraryFrame: boolean }>` +const LineNumberContainer = euiStyled.div<{ isLibraryFrame: boolean }>` position: absolute; top: 0; left: 0; @@ -47,7 +47,7 @@ const LineNumberContainer = styled.div<{ isLibraryFrame: boolean }>` : theme.eui.euiColorLightestShade}; `; -const LineNumber = styled.div<{ highlight: boolean }>` +const LineNumber = euiStyled.div<{ highlight: boolean }>` position: relative; min-width: ${px(units.eighth * 21)}; padding-left: ${px(units.half)}; @@ -64,7 +64,7 @@ const LineNumber = styled.div<{ highlight: boolean }>` } `; -const LineContainer = styled.div` +const LineContainer = euiStyled.div` overflow: auto; margin: 0 0 0 ${px(units.eighth * 21)}; padding: 0; @@ -75,7 +75,7 @@ const LineContainer = styled.div` } `; -const Line = styled.pre` +const Line = euiStyled.pre` // Override all styles margin: 0; color: inherit; @@ -87,7 +87,7 @@ const Line = styled.pre` line-height: ${px(LINE_HEIGHT)}; `; -const Code = styled.code` +const Code = euiStyled.code` position: relative; padding: 0; margin: 0; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx index 636252b19fe39..68b0893e1d8d3 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/FrameHeading.tsx @@ -6,7 +6,7 @@ */ import React, { ComponentType } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { fontFamilyCode, fontSize, px, units } from '../../../style/variables'; import { @@ -18,7 +18,7 @@ import { RubyFrameHeadingRenderer, } from './frame_heading_renderers'; -const FileDetails = styled.div` +const FileDetails = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorDarkShade}; line-height: 1.5; /* matches the line-hight of the accordion container button */ padding: ${px(units.eighth)} 0; @@ -26,12 +26,12 @@ const FileDetails = styled.div` font-size: ${fontSize}; `; -const LibraryFrameFileDetail = styled.span` +const LibraryFrameFileDetail = euiStyled.span` color: ${({ theme }) => theme.eui.euiColorDarkShade}; word-break: break-word; `; -const AppFrameFileDetail = styled.span` +const AppFrameFileDetail = euiStyled.span` color: ${({ theme }) => theme.eui.euiColorFullShade}; word-break: break-word; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx index e67341d68b52f..de417b465638f 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/LibraryStacktrace.tsx @@ -8,12 +8,12 @@ import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { px, units } from '../../../style/variables'; import { Stackframe as StackframeComponent } from './Stackframe'; -const LibraryStacktraceAccordion = styled(EuiAccordion)` +const LibraryStacktraceAccordion = euiStyled(EuiAccordion)` margin: ${px(units.quarter)} 0; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx index 4fd90d343146a..d361634759390 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Stackframe.tsx @@ -7,7 +7,7 @@ import { EuiAccordion } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { Stackframe as StackframeType, StackframeWithLineContext, @@ -22,7 +22,7 @@ import { FrameHeading } from './FrameHeading'; import { Variables } from './Variables'; import { px, units } from '../../../style/variables'; -const ContextContainer = styled.div<{ isLibraryFrame: boolean }>` +const ContextContainer = euiStyled.div<{ isLibraryFrame: boolean }>` position: relative; font-family: ${fontFamilyCode}; font-size: ${fontSize}; @@ -35,7 +35,7 @@ const ContextContainer = styled.div<{ isLibraryFrame: boolean }>` `; // Indent the non-context frames the same amount as the accordion control -const NoContextFrameHeadingWrapper = styled.div` +const NoContextFrameHeadingWrapper = euiStyled.div` margin-left: ${px(units.unit + units.half + units.quarter)}; `; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx index 099611d518d55..7c09048593710 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/Variables.tsx @@ -5,16 +5,16 @@ * 2.0. */ -import styled from 'styled-components'; import { EuiAccordion } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { borderRadius, px, unit, units } from '../../../style/variables'; import { Stackframe } from '../../../../typings/es_schemas/raw/fields/stackframe'; import { KeyValueTable } from '../KeyValueTable'; import { flattenObject } from '../../../utils/flattenObject'; -const VariablesContainer = styled.div` +const VariablesContainer = euiStyled.div` background: ${({ theme }) => theme.eui.euiColorEmptyShade}; border-radius: 0 0 ${borderRadius} ${borderRadius}; padding: ${px(units.half)} ${px(unit)}; diff --git a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx index d07b712e83528..ee764db516d72 100644 --- a/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/StickyProperties/index.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { fontFamilyCode, fontSizes, @@ -25,11 +25,11 @@ export interface IStickyProperty { truncated?: boolean; } -const TooltipFieldName = styled.span` +const TooltipFieldName = euiStyled.span` font-family: ${fontFamilyCode}; `; -const PropertyLabel = styled.div` +const PropertyLabel = euiStyled.div` margin-bottom: ${px(units.half)}; font-size: ${fontSizes.small}; color: ${({ theme }) => theme.eui.euiColorMediumShade}; @@ -41,13 +41,13 @@ const PropertyLabel = styled.div` PropertyLabel.displayName = 'PropertyLabel'; const propertyValueLineHeight = 1.2; -const PropertyValue = styled.div` +const PropertyValue = euiStyled.div` display: inline-block; line-height: ${propertyValueLineHeight}; `; PropertyValue.displayName = 'PropertyValue'; -const PropertyValueTruncated = styled.span` +const PropertyValueTruncated = euiStyled.span` display: inline-block; line-height: ${propertyValueLineHeight}; ${truncate('100%')}; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx index 138afaf256558..ec309f2f74d10 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/ErrorCountSummaryItemBadge.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; import { EuiBadge } from '@elastic/eui'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { useTheme } from '../../../hooks/use_theme'; import { px } from '../../../../public/style/variables'; import { units } from '../../../style/variables'; @@ -17,9 +17,9 @@ interface Props { count: number; } -const Badge = (styled(EuiBadge)` +const Badge = euiStyled(EuiBadge)` margin-top: ${px(units.eighth)}; -` as unknown) as typeof EuiBadge; +`; export function ErrorCountSummaryItemBadge({ count }: Props) { const theme = useTheme(); diff --git a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx index 9e8242dfa2a7d..d72f03c386226 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/HttpInfoSummaryItem/index.tsx @@ -8,15 +8,15 @@ import React from 'react'; import { EuiToolTip, EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { units, px, truncate, unit } from '../../../../style/variables'; import { HttpStatusBadge } from '../HttpStatusBadge'; -const HttpInfoBadge = (styled(EuiBadge)` +const HttpInfoBadge = euiStyled(EuiBadge)` margin-right: ${px(units.quarter)}; -` as unknown) as typeof EuiBadge; +`; -const Url = styled('span')` +const Url = euiStyled('span')` display: inline-block; vertical-align: bottom; ${truncate(px(unit * 24))}; @@ -27,7 +27,7 @@ interface HttpInfoProps { url: string; } -const Span = styled('span')` +const Span = euiStyled('span')` white-space: nowrap; `; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx index 703b0787f7923..20fd19a06c9eb 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/UserAgentSummaryItem.tsx @@ -6,14 +6,14 @@ */ import React from 'react'; -import styled from 'styled-components'; import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { UserAgent } from '../../../../typings/es_schemas/raw/fields/user_agent'; type UserAgentSummaryItemProps = UserAgent; -const Version = styled('span')` +const Version = euiStyled('span')` font-size: ${({ theme }) => theme.eui.euiFontSizeS}; `; diff --git a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx index 357e14ffef356..395156800dceb 100644 --- a/x-pack/plugins/apm/public/components/shared/Summary/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/Summary/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { px, units } from '../../../../public/style/variables'; import { Maybe } from '../../../../typings/common'; @@ -15,7 +15,7 @@ interface Props { items: Array>; } -const Item = styled(EuiFlexItem)` +const Item = euiStyled(EuiFlexItem)` flex-wrap: nowrap; border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; padding-right: ${px(units.half)}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx index 8ce60b58c4c44..f81da48b760e7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Legend/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useTheme } from '../../../../hooks/use_theme'; import { fontSizes, px, units } from '../../../../style/variables'; @@ -22,7 +22,7 @@ interface ContainerProps { disabled: boolean; } -const Container = styled.div` +const Container = euiStyled.div` display: flex; align-items: center; font-size: ${(props) => props.fontSize}; @@ -39,7 +39,7 @@ interface IndicatorProps { withMargin: boolean; } -export const Indicator = styled.span` +export const Indicator = euiStyled.span` width: ${(props) => px(props.radius)}; height: ${(props) => px(props.radius)}; margin-right: ${(props) => (props.withMargin ? px(props.radius / 2) : 0)}; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx index ad8b85ba70c9b..3b7f0fab6c2a7 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/AgentMarker.tsx @@ -7,19 +7,19 @@ import React from 'react'; import { EuiToolTip } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../common/utils/formatters'; import { useTheme } from '../../../../../hooks/use_theme'; import { px, units } from '../../../../../style/variables'; import { Legend } from '../../Legend'; import { AgentMark } from '../../../../app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Marks/get_agent_marks'; -const NameContainer = styled.div` +const NameContainer = euiStyled.div` border-bottom: 1px solid ${({ theme }) => theme.eui.euiColorMediumShade}; padding-bottom: ${px(units.half)}; `; -const TimeContainer = styled.div` +const TimeContainer = euiStyled.div` color: ${({ theme }) => theme.eui.euiColorMediumShade}; padding-top: ${px(units.half)}; `; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx index 393281b2bf848..044070303d2ff 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { EuiPopover, EuiText } from '@elastic/eui'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { asDuration } from '../../../../../../common/utils/formatters'; import { useTheme } from '../../../../../hooks/use_theme'; import { @@ -24,21 +24,21 @@ interface Props { mark: ErrorMark; } -const Popover = styled.div` +const Popover = euiStyled.div` max-width: ${px(280)}; `; -const TimeLegend = styled(Legend)` +const TimeLegend = euiStyled(Legend)` margin-bottom: ${px(unit)}; `; -const ErrorLink = styled(ErrorDetailLink)` +const ErrorLink = euiStyled(ErrorDetailLink)` display: block; margin: ${px(units.half)} 0 ${px(units.half)} 0; overflow-wrap: break-word; `; -const Button = styled(Legend)` +const Button = euiStyled(Legend)` height: 20px; display: flex; align-items: flex-end; diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx index b426a10a7562d..bece72b398d31 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { px } from '../../../../../style/variables'; import { AgentMarker } from './AgentMarker'; import { ErrorMarker } from './ErrorMarker'; @@ -18,7 +18,7 @@ interface Props { x: number; } -const MarkerContainer = styled.div` +const MarkerContainer = euiStyled.div` position: absolute; bottom: 0; `; diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx index cbadbb0cf4f81..a64355e47f757 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/ml_header.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { MLSingleMetricLink } from '../../Links/MachineLearningLinks/MLSingleMetricLink'; @@ -20,14 +20,14 @@ interface Props { mlJobId?: string; } -const ShiftedIconWrapper = styled.span` +const ShiftedIconWrapper = euiStyled.span` padding-right: 5px; position: relative; top: -1px; display: inline-block; `; -const ShiftedEuiText = styled(EuiText)` +const ShiftedEuiText = euiStyled(EuiText)` position: relative; top: 5px; `; diff --git a/x-pack/plugins/apm/public/components/shared/main_tabs.tsx b/x-pack/plugins/apm/public/components/shared/main_tabs.tsx index de4b368efdbbc..941ce924cff07 100644 --- a/x-pack/plugins/apm/public/components/shared/main_tabs.tsx +++ b/x-pack/plugins/apm/public/components/shared/main_tabs.tsx @@ -7,12 +7,12 @@ import { EuiTabs } from '@elastic/eui'; import React, { ReactNode } from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; // Since our `EuiTab` components have `APMLink`s inside of them and not just // `href`s, we need to override the color of the links inside or they will all // be the primary color. -const StyledTabs = styled(EuiTabs)` +const StyledTabs = euiStyled(EuiTabs)` padding: ${({ theme }) => `${theme.eui.gutterTypes.gutterMedium}`}; border-bottom: ${({ theme }) => theme.eui.euiBorderThin}; `; diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 34ba1d86264c1..3285db1f49191 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -7,14 +7,14 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../src/plugins/kibana_react/common'; import { px, unit } from '../../style/variables'; import { DatePicker } from './DatePicker'; import { KueryBar } from './KueryBar'; import { TimeComparison } from './time_comparison'; import { useBreakPoints } from '../../hooks/use_break_points'; -const SearchBarFlexGroup = styled(EuiFlexGroup)` +const SearchBarFlexGroup = euiStyled(EuiFlexGroup)` margin: ${({ theme }) => `${theme.eui.euiSizeS} ${theme.eui.euiSizeS} -${theme.eui.gutterTypes.gutterMedium} ${theme.eui.euiSizeS}`}; `; diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index 02064ea786fb0..e4b03bd57377a 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -10,14 +10,14 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment'; import React from 'react'; import { useHistory } from 'react-router-dom'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { getDateDifference } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { px, unit } from '../../../style/variables'; import * as urlHelpers from '../../shared/Links/url_helpers'; import { useBreakPoints } from '../../../hooks/use_break_points'; -const PrependContainer = styled.div` +const PrependContainer = euiStyled.div` display: flex; justify-content: center; align-items: center; diff --git a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx index c6e939de2b064..63e0b84362073 100644 --- a/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/truncate_with_tooltip/index.tsx @@ -7,12 +7,12 @@ import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; import { truncate } from '../../../style/variables'; const tooltipAnchorClassname = '_apm_truncate_tooltip_anchor_'; -const TooltipWrapper = styled.div` +const TooltipWrapper = euiStyled.div` width: 100%; .${tooltipAnchorClassname} { width: 100% !important; @@ -20,7 +20,7 @@ const TooltipWrapper = styled.div` } `; -const ContentWrapper = styled.div` +const ContentWrapper = euiStyled.div` ${truncate('100%')} `; From a62a229d6f719eea698114676638d2e6c4339757 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Tue, 9 Feb 2021 03:05:38 -0500 Subject: [PATCH 19/19] [Grok Debugger] Changed test to wait for grok debugger container to exist to fix test flakiness (#90543) * Changed the retry to a wait for condition which will keep trying for 20 seconds to see the container. Also changed the data test subject as there was another grokDebugger test subject just in case there was a clash. * Added .only to be able to run the test repeatedly because group 13 is not in the flaky test runner. * Added to group 11 because 13 is not in flaky test runner. Will revert after passing. * Reverted change back to group 13 and removed the comment for the flaky test being skipped. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/grok_debugger/grok_debugger.js | 2 +- .../test/functional/apps/grok_debugger/grok_debugger.js | 3 +-- x-pack/test/functional/services/grok_debugger.js | 8 +++----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js b/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js index aa566b0562802..17a6408298b07 100644 --- a/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js +++ b/x-pack/plugins/grokdebugger/public/components/grok_debugger/grok_debugger.js @@ -129,7 +129,7 @@ export class GrokDebuggerComponent extends React.Component { - + diff --git a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js index 010341cedd3a7..b2a1c5363fcb6 100644 --- a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js +++ b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js @@ -12,8 +12,7 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['grokDebugger']); - // FLAKY: https://github.com/elastic/kibana/issues/84440 - describe.skip('grok debugger app', function () { + describe('grok debugger app', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('empty_kibana'); diff --git a/x-pack/test/functional/services/grok_debugger.js b/x-pack/test/functional/services/grok_debugger.js index 730b4ca60c05a..42a80edd70c85 100644 --- a/x-pack/test/functional/services/grok_debugger.js +++ b/x-pack/test/functional/services/grok_debugger.js @@ -13,7 +13,7 @@ export function GrokDebuggerProvider({ getService }) { const retry = getService('retry'); // test subject selectors - const SUBJ_CONTAINER = 'grokDebugger'; + const SUBJ_CONTAINER = 'grokDebuggerContainer'; const SUBJ_UI_ACE_EVENT_INPUT = `${SUBJ_CONTAINER} > aceEventInput > codeEditorContainer`; const SUBJ_UI_ACE_PATTERN_INPUT = `${SUBJ_CONTAINER} > acePatternInput > codeEditorContainer`; @@ -49,10 +49,8 @@ export function GrokDebuggerProvider({ getService }) { } async assertExists() { - await retry.try(async () => { - if (!(await testSubjects.exists(SUBJ_CONTAINER))) { - throw new Error('Expected to find the grok debugger'); - } + await retry.waitFor('Grok Debugger to exist', async () => { + return await testSubjects.exists(SUBJ_CONTAINER); }); }