From 042c5bc5cee86fec4542a0d9b70bd12a82b01550 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 10 Mar 2021 11:11:08 -0700 Subject: [PATCH 01/26] [Maps] remove EMS tile layers from test artifacts to fix flaky tests (#94319) --- .../apps/maps/auto_fit_to_bounds.js | 3 +-- .../apps/maps/documents_source/search_hits.js | 3 +-- .../apps/maps/visualize_create_menu.js | 5 ++-- .../es_archives/maps/kibana/data.json | 24 +++++++++---------- .../test/functional/page_objects/gis_page.ts | 7 ++++++ 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js index 9847923c1bf5b..8af2e45b59838 100644 --- a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -11,8 +11,7 @@ export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const security = getService('security'); - // FLAKY: https://github.com/elastic/kibana/issues/93737 - describe.skip('auto fit map to bounds', () => { + describe('auto fit map to bounds', () => { describe('initial location', () => { before(async () => { await security.testUser.setRoles(['global_maps_all', 'test_logstash_reader']); diff --git a/x-pack/test/functional/apps/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/documents_source/search_hits.js index 2663242406a75..4da36a44cff08 100644 --- a/x-pack/test/functional/apps/maps/documents_source/search_hits.js +++ b/x-pack/test/functional/apps/maps/documents_source/search_hits.js @@ -84,8 +84,7 @@ export default function ({ getPageObjects, getService }) { expect(beforeQueryRefreshTimestamp).not.to.equal(afterQueryRefreshTimestamp); }); - // https://github.com/elastic/kibana/issues/93718 - it.skip('should apply query to fit to bounds', async () => { + it('should apply query to fit to bounds', async () => { // Set view to other side of world so no matching results await PageObjects.maps.setView(-15, -100, 6); await PageObjects.maps.clickFitToBounds('logstash'); diff --git a/x-pack/test/functional/apps/maps/visualize_create_menu.js b/x-pack/test/functional/apps/maps/visualize_create_menu.js index bac879dd9c81d..c9044353fbde8 100644 --- a/x-pack/test/functional/apps/maps/visualize_create_menu.js +++ b/x-pack/test/functional/apps/maps/visualize_create_menu.js @@ -29,9 +29,8 @@ export default function ({ getService, getPageObjects }) { it('should take users to Maps application when Maps is clicked', async () => { await PageObjects.visualize.clickMapsApp(); await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.maps.waitForLayersToLoad(); - const doesLayerExist = await PageObjects.maps.doesLayerExist('Road map'); - expect(doesLayerExist).to.equal(true); + const onMapPage = await PageObjects.maps.onMapPage(); + expect(onMapPage).to.equal(true); }); }); diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 5d6a355939d30..79f869040f74a 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -150,7 +150,7 @@ "type": "envelope" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "mapStateJSON": "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", "title": "document example", "uiStateJSON": "{\"isDarkMode\":false}" @@ -181,7 +181,7 @@ "type": "envelope" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[\"machine.os\"]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"z52lq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[\"machine.os\"]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "mapStateJSON": "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"machine.os.raw : \\\"ios\\\"\",\"language\":\"kuery\"}}", "title": "document example with query", "uiStateJSON": "{\"isDarkMode\":false}" @@ -212,7 +212,7 @@ "type": "envelope" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[\"machine.os\"]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"z52lq\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[\"machine.os\"]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "description": "", "mapStateJSON": "{\"zoom\":4.09,\"center\":{\"lon\":-100.58836,\"lat\":33.21778},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"index\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"machine.os.raw\",\"value\":\"ios\",\"params\":{\"query\":\"ios\"}},\"query\":{\"match\":{\"machine.os.raw\":{\"query\":\"ios\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}]}", "title": "document example with filter", @@ -288,7 +288,7 @@ "title" : "document example top hits split with scripted field", "description" : "", "mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-24T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]}", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"scalingType\":\"TOP_HITS\",\"topHitsSplitField\":\"hour_of_day\",\"topHitsSize\":1,\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON" : "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"scalingType\":\"TOP_HITS\",\"topHitsSplitField\":\"hour_of_day\",\"topHitsSize\":1,\"sortField\":\"@timestamp\",\"sortOrder\":\"desc\",\"applyGlobalQuery\":true,\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", "bounds" : { "type" : "Polygon", @@ -344,7 +344,7 @@ "title" : "document example with data driven styles", "description" : "", "mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]}", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"field\":{\"label\":\"hour_of_day\",\"name\":\"hour_of_day\",\"origin\":\"source\"}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":4,\"maxSize\":24,\"field\":{\"label\":\"bytes\",\"name\":\"bytes\",\"origin\":\"source\"}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"symbol\":{\"options\":{\"symbolizeAs\":\"circle\",\"symbolId\":\"airfield\"}}}},\"type\":\"VECTOR\"}]", + "layerListJSON" : "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"field\":{\"label\":\"hour_of_day\",\"name\":\"hour_of_day\",\"origin\":\"source\"}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":4,\"maxSize\":24,\"field\":{\"label\":\"bytes\",\"name\":\"bytes\",\"origin\":\"source\"}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"symbol\":{\"options\":{\"symbolizeAs\":\"circle\",\"symbolId\":\"airfield\"}}}},\"type\":\"VECTOR\"}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", "bounds" : { "type" : "Polygon", @@ -400,7 +400,7 @@ "title" : "document example with data driven styles on date field", "description" : "", "mapStateJSON": "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]}", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"field\":{\"label\":\"@timestamp\",\"name\":\"@timestamp\",\"origin\":\"source\"}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":4,\"maxSize\":24,\"field\":{\"label\":\"bytes\",\"name\":\"bytes\",\"origin\":\"source\"}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"symbol\":{\"options\":{\"symbolizeAs\":\"circle\",\"symbolId\":\"airfield\"}}}},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"field\":{\"label\":\"@timestamp\",\"name\":\"@timestamp\",\"origin\":\"source\"}}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":4,\"maxSize\":24,\"field\":{\"label\":\"bytes\",\"name\":\"bytes\",\"origin\":\"source\"}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"symbol\":{\"options\":{\"symbolizeAs\":\"circle\",\"symbolId\":\"airfield\"}}}},\"type\":\"VECTOR\"}]", "uiStateJSON": "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", "bounds" : { "type" : "Polygon", @@ -456,7 +456,7 @@ "title" : "document example hidden", "description" : "", "mapStateJSON" : "{\"zoom\":4.1,\"center\":{\"lon\":-100.61091,\"lat\":33.23887},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":false,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON" : "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":false,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" }, "type" : "map", @@ -681,7 +681,7 @@ "type": "polygon" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"frk92\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"7d807c75-088a-44b7-920a-e7e47f4fc038\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"1035e930-1811-11e9-b78a-23d706cd2507\",\"geoField\":\"location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"frk92\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"7d807c75-088a-44b7-920a-e7e47f4fc038\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"1035e930-1811-11e9-b78a-23d706cd2507\",\"geoField\":\"location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "mapStateJSON": "{\"zoom\":8.8,\"center\":{\"lon\":-179.98743,\"lat\":-0.09561},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"kuery\"}}", "title": "antimeridian points example", "uiStateJSON": "{\"isDarkMode\":false}" @@ -726,7 +726,7 @@ "type": "polygon" }, "description": "", - "layerListJSON": "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"ad9fj\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"4e4b5628-dbdc-40bb-93f0-8a7a48be1141\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"502886a0-18f8-11e9-97c8-5da5e037299c\",\"geoField\":\"location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"id\":\"ad9fj\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"4e4b5628-dbdc-40bb-93f0-8a7a48be1141\",\"type\":\"ES_SEARCH\",\"indexPatternId\":\"502886a0-18f8-11e9-97c8-5da5e037299c\",\"geoField\":\"location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"visible\":true,\"temporary\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", "mapStateJSON": "{\"zoom\":5.65,\"center\":{\"lon\":179.03193,\"lat\":0.09593},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"}}", "title": "antimeridian shapes example", "uiStateJSON": "{\"isDarkMode\":false}" @@ -964,7 +964,7 @@ "title" : "blended document example", "description" : "", "mapStateJSON" : "{\"zoom\":10.27,\"center\":{\"lon\":-83.70716,\"lat\":32.73679},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-23T00:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]}", - "layerListJSON" : "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"43a70a86-00fd-43af-9e84-4d9fe2d7513d\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{},\"type\":\"VECTOR_TILE\"},{\"id\":\"307c8495-89f7-431b-83d8-78724d9a8f72\",\"label\":\"logstash-*\",\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"id\":\"20fc58c3-3c0a-4c7b-9cdc-37552cafdc21\",\"tooltipProperties\":[],\"type\":\"ES_SEARCH\",\"scalingType\":\"CLUSTERS\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"type\":\"BLENDED_VECTOR\",\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true}}]", + "layerListJSON" : "[{\"id\":\"307c8495-89f7-431b-83d8-78724d9a8f72\",\"label\":\"logstash-*\",\"sourceDescriptor\":{\"geoField\":\"geo.coordinates\",\"id\":\"20fc58c3-3c0a-4c7b-9cdc-37552cafdc21\",\"tooltipProperties\":[],\"type\":\"ES_SEARCH\",\"scalingType\":\"CLUSTERS\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"type\":\"BLENDED_VECTOR\",\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"airfield\"}},\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#54B399\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true}}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}", "bounds" : { "type" : "Polygon", @@ -1020,7 +1020,7 @@ "title" : "document example - auto fit to bounds for initial location", "description" : "", "mapStateJSON" : "{\"zoom\":5.2,\"center\":{\"lon\":-67.80052,\"lat\":-55.25331},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"AUTO_FIT_TO_BOUNDS\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", - "layerListJSON" : "[{\"id\":\"0hmz5\",\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"VECTOR_TILE\",\"minZoom\":0,\"maxZoom\":24},{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", + "layerListJSON" : "[{\"id\":\"z52lq\",\"label\":\"logstash\",\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"id\":\"e1a5e1a6-676c-4a89-8ea9-0d91d64b73c6\",\"type\":\"ES_SEARCH\",\"geoField\":\"geo.coordinates\",\"limit\":2048,\"filterByMapBounds\":true,\"showTooltip\":true,\"tooltipProperties\":[],\"applyGlobalQuery\":true,\"scalingType\":\"LIMIT\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#e6194b\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}}},\"previousStyle\":null},\"type\":\"VECTOR\"}]", "uiStateJSON" : "{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" }, "type" : "map", @@ -1047,7 +1047,7 @@ "source": { "map" : { "description":"shapes with mvt scaling", - "layerListJSON":"[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"isAutoSelect\":true},\"id\":\"76b9fc1d-1e8a-4d2f-9f9e-6ba2b19f24bb\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"TILE\"},\"type\":\"VECTOR_TILE\"},{\"sourceDescriptor\":{\"geoField\":\"geometry\",\"filterByMapBounds\":true,\"scalingType\":\"MVT\",\"topHitsSize\":1,\"id\":\"97f8555e-8db0-4bd8-8b18-22e32f468667\",\"type\":\"ES_SEARCH\",\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"caffa63a-ebfb-466d-8ff6-d797975b88ab\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"prop1\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":1},\"type\":\"ORDINAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"TILED_VECTOR\",\"joins\":[]}]", + "layerListJSON":"[{\"sourceDescriptor\":{\"geoField\":\"geometry\",\"filterByMapBounds\":true,\"scalingType\":\"MVT\",\"topHitsSize\":1,\"id\":\"97f8555e-8db0-4bd8-8b18-22e32f468667\",\"type\":\"ES_SEARCH\",\"tooltipProperties\":[],\"sortField\":\"\",\"sortOrder\":\"desc\",\"indexPatternRefName\":\"layer_1_source_index_pattern\"},\"id\":\"caffa63a-ebfb-466d-8ff6-d797975b88ab\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"prop1\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":1},\"type\":\"ORDINAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#41937c\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":6}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"STATIC\",\"options\":{\"value\":\"\"}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"type\":\"TILED_VECTOR\",\"joins\":[]}]", "mapStateJSON":"{\"zoom\":3.75,\"center\":{\"lon\":80.01106,\"lat\":3.65009},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":0},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[],\"settings\":{\"autoFitToDataBounds\":false,\"initialLocation\":\"LAST_SAVED_LOCATION\",\"fixedLocation\":{\"lat\":0,\"lon\":0,\"zoom\":2},\"browserLocation\":{\"zoom\":2},\"maxZoom\":24,\"minZoom\":0,\"showSpatialFilters\":true,\"spatialFiltersAlpa\":0.3,\"spatialFiltersFillColor\":\"#DA8B45\",\"spatialFiltersLineColor\":\"#DA8B45\"}}", "title":"geo_shape_mvt", "uiStateJSON":"{\"isLayerTOCOpen\":true,\"openTOCDetails\":[]}" diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 6694a494cf853..3f6b5691314bb 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -196,6 +196,13 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte return await listingTable.onListingPage('map'); } + async onMapPage() { + log.debug(`onMapPage`); + return await testSubjects.exists('mapLayerTOC', { + timeout: 5000, + }); + } + async searchForMapWithName(name: string) { log.debug(`searchForMapWithName: ${name}`); From 44d8093ddfcbaf65a073b6c67597bfe6c5e4b814 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Wed, 10 Mar 2021 12:15:51 -0600 Subject: [PATCH 02/26] [Field formats] Fix switching away from duration formatter (#93818) * simplify field format editor rendering, fixes switching away from duration formatter --- .../field_format_editor.tsx | 55 ++++++------------- .../apps/management/_field_formatter.js | 53 ++++++++++++++++++ test/functional/apps/management/index.ts | 1 + 3 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 test/functional/apps/management/_field_formatter.js diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx index 1f3e87e69fd4c..155fbc7fd31f5 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx @@ -17,7 +17,6 @@ import { KBN_FIELD_TYPES, ES_FIELD_TYPES, DataPublicPluginStart, - FieldFormat, } from 'src/plugins/data/public'; import { CoreStart } from 'src/core/public'; import { castEsToKbnFieldTypeName } from '../../../../data/public'; @@ -45,7 +44,6 @@ export interface FormatSelectEditorState { fieldTypeFormats: FieldTypeFormat[]; fieldFormatId?: string; fieldFormatParams?: { [key: string]: unknown }; - format: FieldFormat; kbnType: KBN_FIELD_TYPES; } @@ -81,67 +79,48 @@ export class FormatSelectEditor extends PureComponent< > { constructor(props: FormatSelectEditorProps) { super(props); - const { fieldFormats, esTypes, value } = props; + const { fieldFormats, esTypes } = props; const kbnType = castEsToKbnFieldTypeName(esTypes[0] || 'keyword'); - // get current formatter for field, provides default if none exists - const format = value?.id - ? fieldFormats.getInstance(value?.id, value?.params) - : fieldFormats.getDefaultInstance(kbnType, esTypes); - this.state = { fieldTypeFormats: getFieldTypeFormatsList( kbnType, fieldFormats.getDefaultType(kbnType, esTypes) as FieldFormatInstanceType, fieldFormats ), - format, kbnType, }; } - onFormatChange = (formatId: string, params?: any) => { - const { fieldTypeFormats } = this.state; - const { fieldFormats, uiSettings } = this.props; - - const FieldFormatClass = fieldFormats.getType( - formatId || (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.id - ) as FieldFormatInstanceType; - - const newFormat = new FieldFormatClass(params, (key: string) => uiSettings.get(key)); - - this.setState( - { - fieldFormatId: formatId, - fieldFormatParams: params, - format: newFormat, - }, - () => { - this.props.onChange( - formatId - ? { - id: formatId, - params: params || {}, - } - : undefined - ); - } + onFormatChange = (formatId: string, params?: any) => + this.props.onChange( + formatId + ? { + id: formatId, + params: params || {}, + } + : undefined ); - }; + onFormatParamsChange = (newParams: { fieldType: string; [key: string]: any }) => { const { fieldFormatId } = this.state; this.onFormatChange(fieldFormatId as string, newParams); }; render() { - const { fieldFormatEditors, onError, value } = this.props; + const { fieldFormatEditors, onError, value, fieldFormats, esTypes } = this.props; const fieldFormatId = value?.id; const fieldFormatParams = value?.params; const { kbnType } = this.state; - const { fieldTypeFormats, format } = this.state; + const { fieldTypeFormats } = this.state; const defaultFormat = (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.title; + // get current formatter for field, provides default if none exists + const format = value?.id + ? fieldFormats.getInstance(value?.id, value?.params) + : fieldFormats.getDefaultInstance(kbnType, esTypes); + const label = defaultFormat ? ( Date: Wed, 10 Mar 2021 19:49:58 +0100 Subject: [PATCH 03/26] [APM] Refactor agent icon (#91126) --- .../public/components/app/ServiceMap/icons.ts | 4 ++- .../service_icons/icon_popover.tsx | 2 +- .../service_icons/index.test.tsx | 29 ++++++++++++++----- .../service_details/service_icons/index.tsx | 5 +++- .../shared/AgentIcon/get_agent_icon.ts | 20 +++++++++++-- .../components/shared/AgentIcon/icons/php.svg | 27 ++++++----------- .../shared/AgentIcon/icons/php_dark.svg | 9 ++++++ .../shared/AgentIcon/icons/rumjs.svg | 11 +++++-- .../shared/AgentIcon/icons/rumjs_dark.svg | 8 +++++ .../shared/AgentIcon/icons/rust_dark.svg | 6 ++++ .../components/shared/AgentIcon/index.tsx | 10 +++---- .../components/shared/span_icon/index.tsx | 6 ++-- 12 files changed, 95 insertions(+), 42 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php_dark.svg create mode 100644 x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs_dark.svg create mode 100644 x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rust_dark.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts index d4c56bc48c139..da676fd649293 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -19,5 +19,7 @@ export function iconForNode(node: cytoscape.NodeSingular) { const subtype = node.data(SPAN_SUBTYPE); const type = node.data(SPAN_TYPE); - return agentName ? getAgentIcon(agentName) : getSpanIcon(type, subtype); + return agentName + ? getAgentIcon(agentName, false) + : getSpanIcon(type, subtype); } diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx index 4c14a3249fded..f7495d3e51671 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx @@ -44,7 +44,7 @@ export function IconPopover({ ownFocus={false} button={ - + } isOpen={isOpen} diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.test.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.test.tsx index dbf4b65deb3b3..6027e8b1d07c5 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.test.tsx @@ -19,6 +19,7 @@ import { } from '../../../../context/apm_plugin/mock_apm_plugin_context'; import * as fetcherHook from '../../../../hooks/use_fetcher'; import { ServiceIcons } from './'; +import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiCounter: () => {} }, @@ -60,7 +61,9 @@ describe('ServiceIcons', () => { }); const { getByTestId, queryAllByTestId } = render( - + + + ); expect(getByTestId('loading')).toBeInTheDocument(); @@ -77,7 +80,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -96,7 +101,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -116,7 +123,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -137,7 +146,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -180,7 +191,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); @@ -216,7 +229,9 @@ describe('ServiceIcons', () => { const { queryAllByTestId, getByTestId, getByText } = render( - + + + ); expect(queryAllByTestId('loading')).toHaveLength(0); diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx index bb68f74e9846e..6f9c82200fb60 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_icons/index.tsx @@ -8,6 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ReactChild, useState } from 'react'; +import { useTheme } from '../../../../hooks/use_theme'; import { ContainerType } from '../../../../../common/service_metadata'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; @@ -63,6 +64,8 @@ export function ServiceIcons({ serviceName }: Props) { setSelectedIconPopover, ] = useState(); + const theme = useTheme(); + const { data: icons, status: iconsFetchStatus } = useFetcher( (callApmApi) => { if (serviceName && start && end) { @@ -103,7 +106,7 @@ export function ServiceIcons({ serviceName }: Props) { const popoverItems: PopoverItem[] = [ { key: 'service', - icon: getAgentIcon(icons?.agentName) || 'node', + icon: getAgentIcon(icons?.agentName, theme.darkMode) || 'node', isVisible: !!icons?.agentName, title: i18n.translate('xpack.apm.serviceIcons.service', { defaultMessage: 'Service', diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts index 00282c681cbcd..f916292b7f080 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts @@ -22,7 +22,10 @@ import phpIcon from './icons/php.svg'; import pythonIcon from './icons/python.svg'; import rubyIcon from './icons/ruby.svg'; import rumJsIcon from './icons/rumjs.svg'; +import darkPhpIcon from './icons/php_dark.svg'; +import darkRumJsIcon from './icons/rumjs_dark.svg'; import rustIcon from './icons/rust.svg'; +import darkRustIcon from './icons/rust_dark.svg'; const agentIcons: { [key: string]: string } = { dotnet: dotNetIcon, @@ -39,6 +42,13 @@ const agentIcons: { [key: string]: string } = { rust: rustIcon, }; +const darkAgentIcons: { [key: string]: string } = { + ...agentIcons, + php: darkPhpIcon, + rum: darkRumJsIcon, + rust: darkRustIcon, +}; + // This only needs to be exported for testing purposes, since we stub the SVG // import values in test. export function getAgentIconKey(agentName: string) { @@ -66,7 +76,13 @@ export function getAgentIconKey(agentName: string) { } } -export function getAgentIcon(agentName?: string) { +export function getAgentIcon( + agentName: string | undefined, + isDarkMode: boolean +) { const key = agentName && getAgentIconKey(agentName); - return (key && agentIcons[key]) ?? defaultIcon; + if (!key) { + return defaultIcon; + } + return (isDarkMode ? darkAgentIcons[key] : agentIcons[key]) ?? defaultIcon; } diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg index c8af5dc331269..9fc450854d40f 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg @@ -1,18 +1,9 @@ - - - - - - - - - - - - - - - - - - + + + + + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php_dark.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php_dark.svg new file mode 100644 index 0000000000000..e62cf4580198e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php_dark.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg index 87043159ed8c3..4b8cb916b1212 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg @@ -1,3 +1,8 @@ - - - + + + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs_dark.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs_dark.svg new file mode 100644 index 0000000000000..9cb79b0965451 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs_dark.svg @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rust_dark.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rust_dark.svg new file mode 100644 index 0000000000000..e12620f756f52 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rust_dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx index 25abaac82b0a0..f91eb49717782 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx @@ -6,19 +6,19 @@ */ import React from 'react'; +import { EuiIcon } from '@elastic/eui'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; -import { useTheme } from '../../../hooks/use_theme'; import { getAgentIcon } from './get_agent_icon'; +import { useTheme } from '../../../hooks/use_theme'; interface Props { agentName: AgentName; } export function AgentIcon(props: Props) { - const theme = useTheme(); const { agentName } = props; - const size = theme.eui.euiIconSizes.large; - const icon = getAgentIcon(agentName); + const theme = useTheme(); + const icon = getAgentIcon(agentName, theme.darkMode); - return {agentName}; + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx b/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx index db21d781e9eba..05e4067f522a1 100644 --- a/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/span_icon/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useTheme } from '../../../hooks/use_theme'; +import { EuiIcon } from '@elastic/eui'; import { getSpanIcon } from './get_span_icon'; interface Props { @@ -15,9 +15,7 @@ interface Props { } export function SpanIcon({ type, subType }: Props) { - const theme = useTheme(); - const size = theme.eui.euiIconSizes.large; const icon = getSpanIcon(type, subType); - return {type; + return ; } From 672bd95a4830e53e6767fc538fd926bb63a59966 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 10 Mar 2021 12:55:58 -0600 Subject: [PATCH 04/26] [App Search] Add routes for Role Mappings (#94221) * [App Search] Add routes for Role Mappings * Add registering of routes Forgot to port this when cherry picking from another branch. * Add validation --- .../server/routes/app_search/index.ts | 2 + .../routes/app_search/role_mappings.test.ts | 215 ++++++++++++++++++ .../server/routes/app_search/role_mappings.ts | 133 +++++++++++ 3 files changed, 350 insertions(+) create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 90b86138a4a6d..74f13a05aa7e6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -12,6 +12,7 @@ import { registerCredentialsRoutes } from './credentials'; import { registerCurationsRoutes } from './curations'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; import { registerEnginesRoutes } from './engines'; +import { registerRoleMappingsRoutes } from './role_mappings'; import { registerSearchSettingsRoutes } from './search_settings'; import { registerSettingsRoutes } from './settings'; @@ -24,4 +25,5 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerDocumentRoutes(dependencies); registerCurationsRoutes(dependencies); registerSearchSettingsRoutes(dependencies); + registerRoleMappingsRoutes(dependencies); }; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts new file mode 100644 index 0000000000000..53368035af225 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.test.ts @@ -0,0 +1,215 @@ +/* + * 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 { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { + registerRoleMappingsRoute, + registerRoleMappingRoute, + registerNewRoleMappingRoute, + registerResetRoleMappingRoute, +} from './role_mappings'; + +const roleMappingBaseSchema = { + rules: { username: 'user' }, + roleType: 'owner', + engines: ['e1', 'e2'], + accessAllEngines: false, + authProvider: ['*'], +}; + +describe('role mappings routes', () => { + describe('GET /api/app_search/role_mappings', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/role_mappings', + }); + + registerRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings', + }); + }); + }); + + describe('POST /api/app_search/role_mappings', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/role_mappings', + }); + + registerRoleMappingsRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { body: roleMappingBaseSchema }; + mockRouter.shouldValidate(request); + }); + + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); + + describe('GET /api/app_search/role_mappings/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/role_mappings/{id}', + }); + + registerRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/:id', + }); + }); + }); + + describe('PUT /api/app_search/role_mappings/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/app_search/role_mappings/{id}', + }); + + registerRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/:id', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + ...roleMappingBaseSchema, + id: '123', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('missing required fields', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); + + describe('DELETE /api/app_search/role_mappings/{id}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/role_mappings/{id}', + }); + + registerRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/:id', + }); + }); + }); + + describe('GET /api/app_search/role_mappings/new', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/role_mappings/new', + }); + + registerNewRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/new', + }); + }); + }); + + describe('GET /api/app_search/role_mappings/reset', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/role_mappings/reset', + }); + + registerResetRoleMappingRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/role_mappings/reset', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts new file mode 100644 index 0000000000000..4b77c8614a52c --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/role_mappings.ts @@ -0,0 +1,133 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +const roleMappingBaseSchema = { + rules: schema.recordOf(schema.string(), schema.string()), + roleType: schema.string(), + engines: schema.arrayOf(schema.string()), + accessAllEngines: schema.boolean(), + authProvider: schema.arrayOf(schema.string()), +}; + +export function registerRoleMappingsRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/role_mappings', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings', + }) + ); + + router.post( + { + path: '/api/app_search/role_mappings', + validate: { + body: schema.object(roleMappingBaseSchema), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings', + }) + ); +} + +export function registerRoleMappingRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/role_mappings/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/:id', + }) + ); + + router.put( + { + path: '/api/app_search/role_mappings/{id}', + validate: { + body: schema.object({ + ...roleMappingBaseSchema, + id: schema.string(), + }), + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/:id', + }) + ); + + router.delete( + { + path: '/api/app_search/role_mappings/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/:id', + }) + ); +} + +export function registerNewRoleMappingRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/role_mappings/new', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/new', + }) + ); +} + +export function registerResetRoleMappingRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/app_search/role_mappings/reset', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/role_mappings/reset', + }) + ); +} + +export const registerRoleMappingsRoutes = (dependencies: RouteDependencies) => { + registerRoleMappingsRoute(dependencies); + registerRoleMappingRoute(dependencies); + registerNewRoleMappingRoute(dependencies); + registerResetRoleMappingRoute(dependencies); +}; From 7d70ad776a85073873c7b8fdbbc775851d5030be Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 10 Mar 2021 19:59:44 +0100 Subject: [PATCH 05/26] migrate warning mixin to core (#94273) --- .../environment/environment_service.test.ts | 40 +++++++++++--- .../server/environment/environment_service.ts | 10 ++++ src/legacy/server/config/complete.js | 13 ----- src/legacy/server/config/complete.test.js | 54 ------------------- src/legacy/server/kbn_server.js | 6 --- src/legacy/server/warnings/index.js | 19 ------- 6 files changed, 44 insertions(+), 98 deletions(-) delete mode 100644 src/legacy/server/config/complete.js delete mode 100644 src/legacy/server/config/complete.test.js delete mode 100644 src/legacy/server/warnings/index.js diff --git a/src/core/server/environment/environment_service.test.ts b/src/core/server/environment/environment_service.test.ts index efcf349075940..fb3ddaa77b416 100644 --- a/src/core/server/environment/environment_service.test.ts +++ b/src/core/server/environment/environment_service.test.ts @@ -62,18 +62,24 @@ describe('UuidService', () => { let logger: ReturnType; let configService: ReturnType; let coreContext: CoreContext; + let service: EnvironmentService; - beforeEach(() => { - jest.clearAllMocks(); + beforeEach(async () => { logger = loggingSystemMock.create(); configService = getConfigService(); coreContext = mockCoreContext.create({ logger, configService }); + + service = new EnvironmentService(coreContext); + }); + + afterEach(() => { + jest.clearAllMocks(); }); describe('#setup()', () => { it('calls resolveInstanceUuid with correct parameters', async () => { - const service = new EnvironmentService(coreContext); await service.setup(); + expect(resolveInstanceUuid).toHaveBeenCalledTimes(1); expect(resolveInstanceUuid).toHaveBeenCalledWith({ pathConfig, @@ -83,8 +89,8 @@ describe('UuidService', () => { }); it('calls createDataFolder with correct parameters', async () => { - const service = new EnvironmentService(coreContext); await service.setup(); + expect(createDataFolder).toHaveBeenCalledTimes(1); expect(createDataFolder).toHaveBeenCalledWith({ pathConfig, @@ -93,8 +99,8 @@ describe('UuidService', () => { }); it('calls writePidFile with correct parameters', async () => { - const service = new EnvironmentService(coreContext); await service.setup(); + expect(writePidFile).toHaveBeenCalledTimes(1); expect(writePidFile).toHaveBeenCalledWith({ pidConfig, @@ -103,9 +109,31 @@ describe('UuidService', () => { }); it('returns the uuid resolved from resolveInstanceUuid', async () => { - const service = new EnvironmentService(coreContext); const setup = await service.setup(); + expect(setup.instanceUuid).toEqual('SOME_UUID'); }); + + describe('process warnings', () => { + it('logs warnings coming from the process', async () => { + await service.setup(); + + const warning = new Error('something went wrong'); + process.emit('warning', warning); + + expect(logger.get('process').warn).toHaveBeenCalledTimes(1); + expect(logger.get('process').warn).toHaveBeenCalledWith(warning); + }); + + it('does not log deprecation warnings', async () => { + await service.setup(); + + const warning = new Error('something went wrong'); + warning.name = 'DeprecationWarning'; + process.emit('warning', warning); + + expect(logger.get('process').warn).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/src/core/server/environment/environment_service.ts b/src/core/server/environment/environment_service.ts index a6bcdf4c35661..e652622049cfa 100644 --- a/src/core/server/environment/environment_service.ts +++ b/src/core/server/environment/environment_service.ts @@ -30,11 +30,13 @@ export interface InternalEnvironmentServiceSetup { /** @internal */ export class EnvironmentService { private readonly log: Logger; + private readonly processLogger: Logger; private readonly configService: IConfigService; private uuid: string = ''; constructor(core: CoreContext) { this.log = core.logger.get('environment'); + this.processLogger = core.logger.get('process'); this.configService = core.configService; } @@ -50,6 +52,14 @@ export class EnvironmentService { this.log.warn(`Detected an unhandled Promise rejection.\n${reason}`); }); + process.on('warning', (warning) => { + // deprecation warnings do no reflect a current problem for the user and should be filtered out. + if (warning.name === 'DeprecationWarning') { + return; + } + this.processLogger.warn(warning); + }); + await createDataFolder({ pathConfig, logger: this.log }); await writePidFile({ pidConfig, logger: this.log }); diff --git a/src/legacy/server/config/complete.js b/src/legacy/server/config/complete.js deleted file mode 100644 index 5d3b2e55288bb..0000000000000 --- a/src/legacy/server/config/complete.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -export default function (kbnServer, server) { - server.decorate('server', 'config', function () { - return kbnServer.config; - }); -} diff --git a/src/legacy/server/config/complete.test.js b/src/legacy/server/config/complete.test.js deleted file mode 100644 index be12414d77e7b..0000000000000 --- a/src/legacy/server/config/complete.test.js +++ /dev/null @@ -1,54 +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 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 completeMixin from './complete'; -import sinon from 'sinon'; - -describe('server/config completeMixin()', function () { - const sandbox = sinon.createSandbox(); - afterEach(() => sandbox.restore()); - - const setup = (options = {}) => { - const { settings = {}, configValues = {}, disabledPluginSpecs = [], plugins = [] } = options; - - const server = { - decorate: sinon.stub(), - }; - - const config = { - get: sinon.stub().returns(configValues), - }; - - const kbnServer = { - newPlatform: {}, - settings, - server, - config, - disabledPluginSpecs, - plugins, - }; - - const callCompleteMixin = () => completeMixin(kbnServer, server, config); - - return { config, callCompleteMixin, server }; - }; - - describe('server decoration', () => { - it('adds a config() function to the server', async () => { - const { config, callCompleteMixin, server } = setup({ - settings: {}, - configValues: {}, - }); - - await callCompleteMixin(); - sinon.assert.calledOnce(server.decorate); - sinon.assert.calledWith(server.decorate, 'server', 'config', sinon.match.func); - expect(server.decorate.firstCall.args[2]()).toBe(config); - }); - }); -}); diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 34dd700aef414..55593d13d4687 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -15,8 +15,6 @@ import { Config } from './config'; import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; -import warningsMixin from './warnings'; -import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; /** @@ -66,10 +64,6 @@ export default class KbnServer { coreMixin, loggingMixin, - warningsMixin, - - // tell the config we are done loading plugins - configCompleteMixin, // setup routes that serve the @kbn/optimizer output optimizeMixin diff --git a/src/legacy/server/warnings/index.js b/src/legacy/server/warnings/index.js deleted file mode 100644 index d08b9c4219744..0000000000000 --- a/src/legacy/server/warnings/index.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -export default function (kbnServer, server) { - process.on('warning', (warning) => { - // deprecation warnings do no reflect a current problem for - // the user and therefor should be filtered out. - if (warning.name === 'DeprecationWarning') { - return; - } - - server.log(['warning', 'process'], warning); - }); -} From 6044f8a8bf556705dc7a524201f9c5ee7f8b3bdf Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 10 Mar 2021 14:06:47 -0500 Subject: [PATCH 06/26] Removing resolver functional tests (#94331) --- .../apps/endpoint/index.ts | 1 - .../apps/endpoint/resolver.ts | 280 ------------------ .../page_objects/hosts_page.ts | 243 --------------- .../page_objects/index.ts | 2 - 4 files changed, 526 deletions(-) delete mode 100644 x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts delete mode 100644 x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index 4cf931a042221..f28545f83a890 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -32,7 +32,6 @@ export default function (providerContext: FtrProviderContext) { }); loadTestFile(require.resolve('./endpoint_list')); loadTestFile(require.resolve('./policy_details')); - loadTestFile(require.resolve('./resolver')); loadTestFile(require.resolve('./endpoint_telemetry')); loadTestFile(require.resolve('./trusted_apps_list')); loadTestFile(require.resolve('./fleet_integrations')); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts deleted file mode 100644 index 6ab2a3e584eb8..0000000000000 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/resolver.ts +++ /dev/null @@ -1,280 +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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getPageObjects, getService }: FtrProviderContext) { - const pageObjects = getPageObjects(['common', 'timePicker', 'hosts', 'settings']); - const testSubjects = getService('testSubjects'); - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - - /** - * Navigating to the hosts page must be done after data is loaded into ES otherwise - * the hosts page will display the empty default page and if we load data after that - * we'd have to set the source filter on the page. - */ - const navigateToHostsAndSetDate = async () => { - await pageObjects.hosts.navigateToSecurityHostsPage(); - await pageObjects.common.dismissBanner(); - const fromTime = 'Jan 1, 2018 @ 00:00:00.000'; - const toTime = 'now'; - await pageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - }; - - describe.skip('Endpoint Event Resolver', function () { - before(async () => { - await browser.setWindowSize(1800, 1200); - }); - after(async () => { - await pageObjects.hosts.deleteDataStreams(); - }); - - describe('Endpoint Resolver Tree', function () { - before(async () => { - await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/functions', { useCreate: true }); - await navigateToHostsAndSetDate(); - await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.events.file'); - }); - after(async () => { - await pageObjects.hosts.deleteDataStreams(); - }); - - it('check that Resolver and Data table is loaded', async () => { - await testSubjects.existOrFail('resolver:graph'); - await testSubjects.existOrFail('tableHeaderCell_name_0'); - await testSubjects.existOrFail('tableHeaderCell_timestamp_1'); - }); - - it('compare resolver Nodes Table data and Data length', async () => { - const nodeData: string[] = []; - const TableData: string[] = []; - - const Table = await testSubjects.findAll('resolver:node-list:node-link:title'); - for (const value of Table) { - const text = await value._webElement.getText(); - TableData.push(text.split('\n')[0]); - } - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - const Nodes = await testSubjects.findAll('resolver:node:primary-button'); - for (const value of Nodes) { - nodeData.push(await value._webElement.getText()); - } - for (let i = 0; i < nodeData.length; i++) { - expect(TableData[i]).to.eql(nodeData[i]); - } - expect(nodeData.length).to.eql(TableData.length); - await (await testSubjects.find('resolver:graph-controls:zoom-in')).click(); - }); - - it('resolver Nodes navigation Up', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:north-button')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < OriginalNodeDataStyle.length; i++) { - expect(parseFloat(OriginalNodeDataStyle[i].top)).to.lessThan( - parseFloat(NewNodeDataStyle[i].top) - ); - expect(parseFloat(OriginalNodeDataStyle[i].left)).to.equal( - parseFloat(NewNodeDataStyle[i].left) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - }); - - it('resolver Nodes navigation Down', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:south-button')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < NewNodeDataStyle.length; i++) { - expect(parseFloat(NewNodeDataStyle[i].top)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].top) - ); - expect(parseFloat(OriginalNodeDataStyle[i].left)).to.equal( - parseFloat(NewNodeDataStyle[i].left) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - }); - - it('resolver Nodes navigation Left', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:east-button')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < OriginalNodeDataStyle.length; i++) { - expect(parseFloat(NewNodeDataStyle[i].left)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].left) - ); - expect(parseFloat(NewNodeDataStyle[i].top)).to.equal( - parseFloat(OriginalNodeDataStyle[i].top) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - }); - - it('resolver Nodes navigation Right', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await testSubjects.click('resolver:graph-controls:west-button'); - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < NewNodeDataStyle.length; i++) { - expect(parseFloat(OriginalNodeDataStyle[i].left)).to.lessThan( - parseFloat(NewNodeDataStyle[i].left) - ); - expect(parseFloat(NewNodeDataStyle[i].top)).to.equal( - parseFloat(OriginalNodeDataStyle[i].top) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - }); - - it('resolver Nodes navigation Center', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:east-button')).click(); - await (await testSubjects.find('resolver:graph-controls:south-button')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 0; i < NewNodeDataStyle.length; i++) { - expect(parseFloat(NewNodeDataStyle[i].left)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].left) - ); - expect(parseFloat(NewNodeDataStyle[i].top)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].top) - ); - } - await (await testSubjects.find('resolver:graph-controls:center-button')).click(); - const CenterNodeDataStyle = await pageObjects.hosts.parseStyles(); - - for (let i = 0; i < CenterNodeDataStyle.length; i++) { - expect(parseFloat(CenterNodeDataStyle[i].left)).to.equal( - parseFloat(OriginalNodeDataStyle[i].left) - ); - expect(parseFloat(CenterNodeDataStyle[i].top)).to.equal( - parseFloat(OriginalNodeDataStyle[i].top) - ); - } - }); - - it('resolver Nodes navigation zoom in', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:zoom-in')).click(); - - const NewNodeDataStyle = await pageObjects.hosts.parseStyles(); - for (let i = 1; i < NewNodeDataStyle.length; i++) { - expect(parseFloat(NewNodeDataStyle[i].left)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].left) - ); - expect(parseFloat(NewNodeDataStyle[i].top)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].top) - ); - expect(parseFloat(OriginalNodeDataStyle[i].width)).to.lessThan( - parseFloat(NewNodeDataStyle[i].width) - ); - expect(parseFloat(OriginalNodeDataStyle[i].height)).to.lessThan( - parseFloat(NewNodeDataStyle[i].height) - ); - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - } - }); - - it('resolver Nodes navigation zoom out', async () => { - const OriginalNodeDataStyle = await pageObjects.hosts.parseStyles(); - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - const NewNodeDataStyle1 = await pageObjects.hosts.parseStyles(); - for (let i = 1; i < OriginalNodeDataStyle.length; i++) { - expect(parseFloat(OriginalNodeDataStyle[i].left)).to.lessThan( - parseFloat(NewNodeDataStyle1[i].left) - ); - expect(parseFloat(OriginalNodeDataStyle[i].top)).to.lessThan( - parseFloat(NewNodeDataStyle1[i].top) - ); - expect(parseFloat(NewNodeDataStyle1[i].width)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].width) - ); - expect(parseFloat(NewNodeDataStyle1[i].height)).to.lessThan( - parseFloat(OriginalNodeDataStyle[i].height) - ); - } - await (await testSubjects.find('resolver:graph-controls:zoom-in')).click(); - }); - }); - - describe('node related event pills', function () { - /** - * Verifies that the pills of a node have the correct text. - * - * @param id the node ID to verify the pills for. - * @param expectedPills a map of expected pills for all nodes - */ - const verifyPills = async (id: string, expectedPills: Set) => { - const relatedEventPills = await pageObjects.hosts.findNodePills(id); - expect(relatedEventPills.length).to.equal(expectedPills.size); - for (const pill of relatedEventPills) { - const pillText = await pill._webElement.getText(); - // check that we have the pill text in our expected map - expect(expectedPills.has(pillText)).to.equal(true); - } - }; - - before(async () => { - await esArchiver.load('empty_kibana'); - await esArchiver.load('endpoint/resolver_tree/alert_events', { useCreate: true }); - await navigateToHostsAndSetDate(); - }); - after(async () => { - await pageObjects.hosts.deleteDataStreams(); - }); - - describe('endpoint.alerts filter', () => { - before(async () => { - await pageObjects.hosts.executeQueryAndOpenResolver('event.dataset : endpoint.alerts'); - await pageObjects.hosts.clickZoomOut(); - await browser.setWindowSize(2100, 1500); - }); - - it('has the correct pill text', async () => { - const expectedData: Map> = new Map([ - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTc2MzYtMTMyNDc2MTQ0NDIuOTU5MTE2NjAw', - new Set(['1 library']), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTMxMTYtMTMyNDcyNDk0MjQuOTg4ODI4NjAw', - new Set(['157 file', '520 registry']), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTUwODQtMTMyNDc2MTQ0NDIuOTcyODQ3MjAw', - new Set(), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTg2OTYtMTMyNDc2MTQ0MjEuNjc1MzY0OTAw', - new Set(['3 file']), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTcyNjAtMTMyNDc2MTQ0MjIuMjQwNDI2MTAw', - new Set(), - ], - [ - 'MTk0YzBmOTgtNjA4My1jNWE4LTYzNjYtZjVkNzI2YWU2YmIyLTczMDAtMTMyNDc2MTQ0MjEuNjg2NzI4NTAw', - new Set(), - ], - ]); - - for (const [id, expectedPills] of expectedData.entries()) { - // center the node in the view - await pageObjects.hosts.clickNodeLinkInPanel(id); - await verifyPills(id, expectedPills); - } - }); - }); - }); - }); -} diff --git a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts b/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts deleted file mode 100644 index e7553e68d670b..0000000000000 --- a/x-pack/test/security_solution_endpoint/page_objects/hosts_page.ts +++ /dev/null @@ -1,243 +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 { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; -import { nudgeAnimationDuration } from '../../../plugins/security_solution/public/resolver/store/camera/scaling_constants'; -import { FtrProviderContext } from '../ftr_provider_context'; -import { - deleteEventsStream, - deleteAlertsStream, - deleteMetadataStream, - deletePolicyStream, - deleteTelemetryStream, -} from '../../security_solution_endpoint_api_int/apis/data_stream_helper'; - -export interface DataStyle { - left: string; - top: string; - width: string; - height: string; -} - -export function SecurityHostsPageProvider({ getService, getPageObjects }: FtrProviderContext) { - const pageObjects = getPageObjects(['common', 'header']); - const testSubjects = getService('testSubjects'); - const queryBar = getService('queryBar'); - const find = getService('find'); - - /** - * Returns the node IDs for the visible nodes in the resolver graph. - */ - const findVisibleNodeIDs = async (): Promise => { - const visibleNodes = await testSubjects.findAll('resolver:node'); - return Promise.all( - visibleNodes.map(async (node: WebElementWrapper) => { - return node.getAttribute('data-test-resolver-node-id'); - }) - ); - }; - - /** - * This assumes you are on the process list in the panel and will find and click the node - * with the given ID to bring it into view in the graph. - * - * @param id the ID of the node to find and click. - */ - const clickNodeLinkInPanel = async (id: string): Promise => { - await navigateToProcessListInPanel(); - const panelNodeButton = await find.byCssSelector( - `[data-test-subj='resolver:node-list:node-link'][data-test-node-id='${id}']` - ); - - await panelNodeButton?.click(); - // ensure that we wait longer than the animation time - await pageObjects.common.sleep(nudgeAnimationDuration * 2); - }; - - /** - * Finds all the pills for a particular node. - * - * @param id the ID of the node - */ - const findNodePills = async (id: string): Promise => { - return testSubjects.findAllDescendant( - 'resolver:map:node-submenu-item', - await find.byCssSelector( - `[data-test-subj='resolver:node'][data-test-resolver-node-id='${id}']` - ) - ); - }; - - /** - * Navigate back to the process list view in the panel. - */ - const navigateToProcessListInPanel = async () => { - const [ - isOnNodeListPage, - isOnCategoryPage, - isOnNodeDetailsPage, - isOnRelatedEventDetailsPage, - ] = await Promise.all([ - testSubjects.exists('resolver:node-list', { timeout: 1 }), - testSubjects.exists('resolver:node-events-in-category:breadcrumbs:node-list-link', { - timeout: 1, - }), - testSubjects.exists('resolver:node-detail:breadcrumbs:node-list-link', { timeout: 1 }), - testSubjects.exists('resolver:event-detail:breadcrumbs:node-list-link', { timeout: 1 }), - ]); - - if (isOnNodeListPage) { - return; - } else if (isOnCategoryPage) { - await ( - await testSubjects.find('resolver:node-events-in-category:breadcrumbs:node-list-link') - ).click(); - } else if (isOnNodeDetailsPage) { - await (await testSubjects.find('resolver:node-detail:breadcrumbs:node-list-link')).click(); - } else if (isOnRelatedEventDetailsPage) { - await (await testSubjects.find('resolver:event-detail:breadcrumbs:node-list-link')).click(); - } else { - // unknown page - return; - } - - await pageObjects.common.sleep(100); - }; - - /** - * Click the zoom out control. - */ - const clickZoomOut = async () => { - await (await testSubjects.find('resolver:graph-controls:zoom-out')).click(); - }; - - /** - * Navigate to Events Panel - */ - const navigateToEventsPanel = async () => { - const isFullScreen = await testSubjects.exists('exit-full-screen', { timeout: 400 }); - if (isFullScreen) { - await (await testSubjects.find('exit-full-screen')).click(); - } - - if (!(await testSubjects.exists('investigate-in-resolver-button', { timeout: 400 }))) { - await (await testSubjects.find('navigation-hosts')).click(); - await testSubjects.click('navigation-events'); - await testSubjects.existOrFail('event'); - } - }; - - /** - * @function parseStyles - * Parses a string of inline styles into a typescript object with casing for react - * @param {string} styles - * @returns {Object} - */ - const parseStyle = ( - styles: string - ): { - left?: string; - top?: string; - width?: string; - height?: string; - } => - styles - .split(';') - .filter((style: string) => style.split(':')[0] && style.split(':')[1]) - .map((style: string) => [ - style - .split(':')[0] - .trim() - .replace(/-./g, (c: string) => c.substr(1).toUpperCase()), - style.split(':').slice(1).join(':').trim(), - ]) - .reduce( - (styleObj: {}, style: string[]) => ({ - ...styleObj, - [style[0]]: style[1], - }), - {} - ); - - /** - * Navigate to the Security Hosts page - */ - const navigateToSecurityHostsPage = async () => { - await pageObjects.common.navigateToUrlWithBrowserHistory('security', '/hosts/AllHosts'); - await pageObjects.header.waitUntilLoadingHasFinished(); - }; - - /** - * Finds a table and returns the data in a nested array with row 0 is the headers if they exist. - * It uses euiTableCellContent to avoid polluting the array data with the euiTableRowCell__mobileHeader data. - * @param dataTestSubj - * @param element - * @returns Promise - */ - const getEndpointEventResolverNodeData = async (dataTestSubj: string, element: string) => { - await testSubjects.exists(dataTestSubj); - const Elements = await testSubjects.findAll(dataTestSubj); - const $ = []; - for (const value of Elements) { - $.push(await value.getAttribute(element)); - } - return $; - }; - - /** - * Gets a array of not parsed styles and returns the Array of parsed styles. - * @returns Promise - */ - const parseStyles = async () => { - const tableData = await getEndpointEventResolverNodeData('resolver:node', 'style'); - const styles: DataStyle[] = []; - for (let i = 1; i < tableData.length; i++) { - const eachStyle = parseStyle(tableData[i]); - styles.push({ - top: eachStyle.top ?? '', - height: eachStyle.height ?? '', - left: eachStyle.left ?? '', - width: eachStyle.width ?? '', - }); - } - return styles; - }; - /** - * Deletes DataStreams from Index Management. - */ - const deleteDataStreams = async () => { - await deleteEventsStream(getService); - await deleteAlertsStream(getService); - await deletePolicyStream(getService); - await deleteMetadataStream(getService); - await deleteTelemetryStream(getService); - }; - - /** - * execute Query And Open Resolver - */ - const executeQueryAndOpenResolver = async (query: string) => { - await navigateToEventsPanel(); - await queryBar.setQuery(query); - await queryBar.submitQuery(); - await testSubjects.click('full-screen'); - await testSubjects.click('investigate-in-resolver-button'); - }; - - return { - navigateToProcessListInPanel, - findNodePills, - clickNodeLinkInPanel, - findVisibleNodeIDs, - clickZoomOut, - navigateToEventsPanel, - navigateToSecurityHostsPage, - parseStyles, - deleteDataStreams, - executeQueryAndOpenResolver, - }; -} diff --git a/x-pack/test/security_solution_endpoint/page_objects/index.ts b/x-pack/test/security_solution_endpoint/page_objects/index.ts index 44c4bb21787a4..961e5ae44716d 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/index.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/index.ts @@ -11,7 +11,6 @@ import { EndpointPolicyPageProvider } from './policy_page'; import { TrustedAppsPageProvider } from './trusted_apps_page'; import { EndpointPageUtils } from './page_utils'; import { IngestManagerCreatePackagePolicy } from './ingest_manager_create_package_policy_page'; -import { SecurityHostsPageProvider } from './hosts_page'; import { FleetIntegrations } from './fleet_integrations_page'; export const pageObjects = { @@ -21,6 +20,5 @@ export const pageObjects = { trustedApps: TrustedAppsPageProvider, endpointPageUtils: EndpointPageUtils, ingestManagerCreatePackagePolicy: IngestManagerCreatePackagePolicy, - hosts: SecurityHostsPageProvider, fleetIntegrations: FleetIntegrations, }; From 7d2c4d4d09fd9c9ec4b03d643d710c2147ef92a8 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Wed, 10 Mar 2021 20:21:39 +0100 Subject: [PATCH 07/26] remove `try` auth mode (#94287) * remove `try` auth mode * update generated doc * update generated doc * adapt integration test --- api_docs/actions.json | 104 +++++++++--------- api_docs/core.json | 80 +++++++------- api_docs/core_http.json | 24 ++-- api_docs/lists.json | 78 ++++++++++++- api_docs/lists.mdx | 3 + api_docs/vis_type_timeseries.json | 20 +--- .../kibana-plugin-core-server.authtoolkit.md | 2 +- ...ugin-core-server.authtoolkit.nothandled.md | 2 +- ...-server.routeconfigoptions.authrequired.md | 4 +- ...a-plugin-core-server.routeconfigoptions.md | 2 +- src/core/server/http/http_server.ts | 7 +- .../integration_tests/core_services.test.ts | 31 ------ .../http/integration_tests/http_auth.test.ts | 53 +-------- .../http/integration_tests/router.test.ts | 19 +++- src/core/server/http/lifecycle/auth.ts | 2 +- src/core/server/http/router/route.ts | 6 +- .../bootstrap/register_bootstrap_route.ts | 2 +- src/core/server/server.api.md | 2 +- 18 files changed, 219 insertions(+), 222 deletions(-) diff --git a/api_docs/actions.json b/api_docs/actions.json index fb9bafd6def94..ec2bd86581f32 100644 --- a/api_docs/actions.json +++ b/api_docs/actions.json @@ -127,7 +127,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 70 + "lineNumber": 63 } }, { @@ -138,7 +138,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 71 + "lineNumber": 64 } }, { @@ -149,7 +149,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 72 + "lineNumber": 65 } }, { @@ -160,7 +160,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 73 + "lineNumber": 66 }, "signature": [ "Config | undefined" @@ -174,13 +174,13 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 74 + "lineNumber": 67 } } ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 69 + "lineNumber": 62 }, "initialIsOpen": false }, @@ -199,7 +199,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 47 + "lineNumber": 40 }, "signature": [ "() => ", @@ -220,7 +220,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 48 + "lineNumber": 41 }, "signature": [ "() => ", @@ -237,7 +237,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 46 + "lineNumber": 39 }, "initialIsOpen": false }, @@ -256,7 +256,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 56 + "lineNumber": 49 }, "signature": [ { @@ -276,7 +276,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 57 + "lineNumber": 50 }, "signature": [ { @@ -291,7 +291,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 55 + "lineNumber": 48 }, "initialIsOpen": false }, @@ -320,7 +320,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 108 + "lineNumber": 101 } }, { @@ -331,7 +331,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 109 + "lineNumber": 102 } }, { @@ -342,7 +342,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 110 + "lineNumber": 103 }, "signature": [ "number | undefined" @@ -356,7 +356,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 111 + "lineNumber": 104 }, "signature": [ "\"basic\" | \"standard\" | \"gold\" | \"platinum\" | \"enterprise\" | \"trial\"" @@ -370,7 +370,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 112 + "lineNumber": 105 }, "signature": [ "{ params?: ValidatorType | undefined; config?: ValidatorType | undefined; secrets?: ValidatorType | undefined; } | undefined" @@ -395,7 +395,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 117 + "lineNumber": 110 } }, { @@ -408,7 +408,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 117 + "lineNumber": 110 } } ], @@ -416,7 +416,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 117 + "lineNumber": 110 } }, { @@ -427,7 +427,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 118 + "lineNumber": 111 }, "signature": [ { @@ -443,7 +443,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 102 + "lineNumber": 95 }, "initialIsOpen": false }, @@ -472,7 +472,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 62 + "lineNumber": 55 } }, { @@ -483,7 +483,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 63 + "lineNumber": 56 }, "signature": [ { @@ -503,7 +503,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 64 + "lineNumber": 57 }, "signature": [ "Config" @@ -517,7 +517,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 65 + "lineNumber": 58 }, "signature": [ "Secrets" @@ -531,7 +531,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 66 + "lineNumber": 59 }, "signature": [ "Params" @@ -540,7 +540,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 61 + "lineNumber": 54 }, "initialIsOpen": false }, @@ -577,7 +577,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 81 + "lineNumber": 74 }, "signature": [ "Secrets" @@ -586,7 +586,7 @@ ], "source": { "path": "x-pack/plugins/actions/server/types.ts", - "lineNumber": 77 + "lineNumber": 70 }, "initialIsOpen": false } @@ -1008,7 +1008,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 86 + "lineNumber": 85 } } ], @@ -1016,13 +1016,13 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 80 + "lineNumber": 79 } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 79 + "lineNumber": 78 }, "lifecycle": "setup", "initialIsOpen": true @@ -1053,7 +1053,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 91 + "lineNumber": 90 } }, { @@ -1071,13 +1071,13 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 91 + "lineNumber": 90 } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 91 + "lineNumber": 90 } } ], @@ -1085,7 +1085,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 91 + "lineNumber": 90 } }, { @@ -1107,7 +1107,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 93 + "lineNumber": 92 } }, { @@ -1120,7 +1120,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 94 + "lineNumber": 93 } }, { @@ -1138,13 +1138,13 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 95 + "lineNumber": 94 } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 95 + "lineNumber": 94 } } ], @@ -1152,7 +1152,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 92 + "lineNumber": 91 } }, { @@ -1197,7 +1197,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 97 + "lineNumber": 96 } } ], @@ -1205,7 +1205,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 97 + "lineNumber": 96 } }, { @@ -1250,7 +1250,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 98 + "lineNumber": 97 } } ], @@ -1258,7 +1258,7 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 98 + "lineNumber": 97 } }, { @@ -1269,7 +1269,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 99 + "lineNumber": 98 }, "signature": [ { @@ -1301,7 +1301,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 101 + "lineNumber": 100 } }, { @@ -1314,7 +1314,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 102 + "lineNumber": 101 } }, { @@ -1327,7 +1327,7 @@ "description": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 103 + "lineNumber": 102 } } ], @@ -1335,13 +1335,13 @@ "returnComment": [], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 100 + "lineNumber": 99 } } ], "source": { "path": "x-pack/plugins/actions/server/plugin.ts", - "lineNumber": 90 + "lineNumber": 89 }, "lifecycle": "start", "initialIsOpen": true diff --git a/api_docs/core.json b/api_docs/core.json index 5446492e0e863..ba8f27d50d13c 100644 --- a/api_docs/core.json +++ b/api_docs/core.json @@ -6622,14 +6622,6 @@ "label": "asScoped", "signature": [ "(request?: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -6645,6 +6637,14 @@ "section": "def-server.LegacyRequest", "text": "LegacyRequest" }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" + }, " | undefined) => Pick<", { "pluginId": "core", @@ -6664,14 +6664,6 @@ "label": "request", "isRequired": false, "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -6687,6 +6679,14 @@ "section": "def-server.LegacyRequest", "text": "LegacyRequest" }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" + }, " | undefined" ], "description": [ @@ -16174,14 +16174,6 @@ }, "signature": [ "{ callAsInternalUser: LegacyAPICaller; asScoped: (request?: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -16197,6 +16189,14 @@ "section": "def-server.LegacyRequest", "text": "LegacyRequest" }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" + }, " | undefined) => Pick; }" ], "initialIsOpen": false @@ -16218,14 +16218,6 @@ }, "signature": [ "{ close: () => void; callAsInternalUser: LegacyAPICaller; asScoped: (request?: ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -16241,6 +16233,14 @@ "section": "def-server.LegacyRequest", "text": "LegacyRequest" }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" + }, " | undefined) => Pick; }" ], "initialIsOpen": false @@ -16533,14 +16533,6 @@ "lineNumber": 192 }, "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" - }, - " | ", { "pluginId": "core", "scope": "server", @@ -16555,6 +16547,14 @@ "docId": "kibCoreHttpPluginApi", "section": "def-server.LegacyRequest", "text": "LegacyRequest" + }, + " | ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCorePluginApi", + "section": "def-server.FakeRequest", + "text": "FakeRequest" } ], "initialIsOpen": false diff --git a/api_docs/core_http.json b/api_docs/core_http.json index c4a5bdd464c98..8053550cc0e80 100644 --- a/api_docs/core_http.json +++ b/api_docs/core_http.json @@ -2887,7 +2887,7 @@ "type": "Function", "label": "notHandled", "description": [ - "\nUser has no credentials.\nAllows user to access a resource when authRequired is 'optional' or 'try'\nRejects a request when authRequired: true" + "\nUser has no credentials.\nAllows user to access a resource when authRequired is 'optional'\nRejects a request when authRequired: true" ], "source": { "path": "src/core/server/http/lifecycle/auth.ts", @@ -4648,7 +4648,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 173 + "lineNumber": 171 } }, { @@ -4661,7 +4661,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 231 + "lineNumber": 229 }, "signature": [ "false | ", @@ -4685,7 +4685,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 236 + "lineNumber": 234 }, "signature": [ { @@ -4701,7 +4701,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 159 + "lineNumber": 157 }, "initialIsOpen": false }, @@ -4732,14 +4732,14 @@ "type": "CompoundType", "label": "authRequired", "description": [ - "\nDefines authentication mode for a route:\n- true. A user has to have valid credentials to access a resource\n- false. A user can access a resource without any credentials.\n- 'optional'. A user can access a resource if has valid credentials or no credentials at all.\n Can be useful when we grant access to a resource but want to identify a user if possible.\n- 'try'. A user can access a resource with valid, invalid or without any credentials.\n Users with valid credentials will be authenticated\n\nDefaults to `true` if an auth mechanism is registered." + "\nDefines authentication mode for a route:\n- true. A user has to have valid credentials to access a resource\n- false. A user can access a resource without any credentials.\n- 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid.\n Can be useful when we grant access to a resource but want to identify a user if possible.\n\nDefaults to `true` if an auth mechanism is registered." ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 118 + "lineNumber": 116 }, "signature": [ - "boolean | \"optional\" | \"try\" | undefined" + "boolean | \"optional\" | undefined" ] }, { @@ -4752,7 +4752,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 127 + "lineNumber": 125 }, "signature": [ "(Method extends \"get\" ? never : boolean) | undefined" @@ -4768,7 +4768,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 132 + "lineNumber": 130 }, "signature": [ "readonly string[] | undefined" @@ -4784,7 +4784,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 137 + "lineNumber": 135 }, "signature": [ "(Method extends ", @@ -4816,7 +4816,7 @@ ], "source": { "path": "src/core/server/http/router/route.ts", - "lineNumber": 142 + "lineNumber": 140 }, "signature": [ "{ payload?: (Method extends ", diff --git a/api_docs/lists.json b/api_docs/lists.json index 077ea74fdff28..8c6639e0ac85e 100644 --- a/api_docs/lists.json +++ b/api_docs/lists.json @@ -3706,7 +3706,83 @@ }, "common": { "classes": [], - "functions": [], + "functions": [ + { + "id": "def-common.buildExceptionFilter", + "type": "Function", + "children": [ + { + "id": "def-common.buildExceptionFilter.{\n- lists,\n excludeExceptions,\n chunkSize,\n}", + "type": "Object", + "label": "{\n lists,\n excludeExceptions,\n chunkSize,\n}", + "tags": [], + "description": [], + "children": [ + { + "tags": [], + "id": "def-common.buildExceptionFilter.{\n- lists,\n excludeExceptions,\n chunkSize,\n}.lists", + "type": "Array", + "label": "lists", + "description": [], + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 74 + }, + "signature": [ + "(({ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"long\" | \"double\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"short\" | \"binary\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"double_range\" | \"float_range\" | \"half_float\" | \"integer_range\" | \"long_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; })[]; field: string; type: \"nested\"; })[]; list_id: string; name: string; type: \"simple\"; } & { comments?: { comment: string; }[] | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; }) | { _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"long\" | \"double\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"short\" | \"binary\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"double_range\" | \"float_range\" | \"half_float\" | \"integer_range\" | \"long_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; })[]; field: string; type: \"nested\"; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; })[]" + ] + }, + { + "tags": [], + "id": "def-common.buildExceptionFilter.{\n- lists,\n excludeExceptions,\n chunkSize,\n}.excludeExceptions", + "type": "boolean", + "label": "excludeExceptions", + "description": [], + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 75 + } + }, + { + "tags": [], + "id": "def-common.buildExceptionFilter.{\n- lists,\n excludeExceptions,\n chunkSize,\n}.chunkSize", + "type": "number", + "label": "chunkSize", + "description": [], + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 76 + } + } + ], + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 73 + } + } + ], + "signature": [ + "({ lists, excludeExceptions, chunkSize, }: { lists: (({ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"long\" | \"double\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"short\" | \"binary\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"double_range\" | \"float_range\" | \"half_float\" | \"integer_range\" | \"long_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; })[]; field: string; type: \"nested\"; })[]; list_id: string; name: string; type: \"simple\"; } & { comments?: { comment: string; }[] | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; }) | { _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"ip\" | \"long\" | \"double\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"short\" | \"binary\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"double_range\" | \"float_range\" | \"half_float\" | \"integer_range\" | \"long_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; })[]; field: string; type: \"nested\"; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; })[]; excludeExceptions: boolean; chunkSize: number; }) => ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + " | undefined" + ], + "description": [], + "label": "buildExceptionFilter", + "source": { + "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", + "lineNumber": 69 + }, + "tags": [], + "returnComment": [], + "initialIsOpen": false + } + ], "interfaces": [], "enums": [ { diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 8e4a8efb7c24e..5d5f771548355 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -41,6 +41,9 @@ import listsObj from './lists.json'; ### Objects +### Functions + + ### Enums diff --git a/api_docs/vis_type_timeseries.json b/api_docs/vis_type_timeseries.json index 657e9a560060e..907ced500294a 100644 --- a/api_docs/vis_type_timeseries.json +++ b/api_docs/vis_type_timeseries.json @@ -30,7 +30,7 @@ "description": [], "source": { "path": "src/plugins/vis_type_timeseries/server/plugin.ts", - "lineNumber": 48 + "lineNumber": 53 }, "signature": [ "(requestContext: ", @@ -45,19 +45,11 @@ { "pluginId": "core", "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.FakeRequest", - "text": "FakeRequest" + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" }, - ", options: ", - { - "pluginId": "visTypeTimeseries", - "scope": "server", - "docId": "kibVisTypeTimeseriesPluginApi", - "section": "def-server.GetVisDataOptions", - "text": "GetVisDataOptions" - }, - ") => Promise<", + ", options: any) => Promise<", { "pluginId": "visTypeTimeseries", "scope": "common", @@ -71,7 +63,7 @@ ], "source": { "path": "src/plugins/vis_type_timeseries/server/plugin.ts", - "lineNumber": 47 + "lineNumber": 52 }, "lifecycle": "setup", "initialIsOpen": true diff --git a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md index 0f0b070dbe87e..5f8b98ab2e894 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.md @@ -17,6 +17,6 @@ export interface AuthToolkit | Property | Type | Description | | --- | --- | --- | | [authenticated](./kibana-plugin-core-server.authtoolkit.authenticated.md) | (data?: AuthResultParams) => AuthResult | Authentication is successful with given credentials, allow request to pass through | -| [notHandled](./kibana-plugin-core-server.authtoolkit.nothandled.md) | () => AuthResult | User has no credentials. Allows user to access a resource when authRequired is 'optional' or 'try' Rejects a request when authRequired: true | +| [notHandled](./kibana-plugin-core-server.authtoolkit.nothandled.md) | () => AuthResult | User has no credentials. Allows user to access a resource when authRequired is 'optional' Rejects a request when authRequired: true | | [redirected](./kibana-plugin-core-server.authtoolkit.redirected.md) | (headers: {
location: string;
} & ResponseHeaders) => AuthResult | Redirects user to another location to complete authentication when authRequired: true Allows user to access a resource without redirection when authRequired: 'optional' | diff --git a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.nothandled.md b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.nothandled.md index 7dc3b47e27e18..577faa6562558 100644 --- a/docs/development/core/server/kibana-plugin-core-server.authtoolkit.nothandled.md +++ b/docs/development/core/server/kibana-plugin-core-server.authtoolkit.nothandled.md @@ -4,7 +4,7 @@ ## AuthToolkit.notHandled property -User has no credentials. Allows user to access a resource when authRequired is 'optional' or 'try' Rejects a request when authRequired: true +User has no credentials. Allows user to access a resource when authRequired is 'optional' Rejects a request when authRequired: true Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.authrequired.md b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.authrequired.md index 9f3822e5c206b..28f712316bc36 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.authrequired.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.authrequired.md @@ -4,12 +4,12 @@ ## RouteConfigOptions.authRequired property -Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible. - 'try'. A user can access a resource with valid, invalid or without any credentials. Users with valid credentials will be authenticated +Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid. Can be useful when we grant access to a resource but want to identify a user if possible. Defaults to `true` if an auth mechanism is registered. Signature: ```typescript -authRequired?: boolean | 'optional' | 'try'; +authRequired?: boolean | 'optional'; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md index bd53570becf63..cf0fe32c14d1d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.routeconfigoptions.md @@ -16,7 +16,7 @@ export interface RouteConfigOptions | Property | Type | Description | | --- | --- | --- | -| [authRequired](./kibana-plugin-core-server.routeconfigoptions.authrequired.md) | boolean | 'optional' | 'try' | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource if has valid credentials or no credentials at all. Can be useful when we grant access to a resource but want to identify a user if possible. - 'try'. A user can access a resource with valid, invalid or without any credentials. Users with valid credentials will be authenticatedDefaults to true if an auth mechanism is registered. | +| [authRequired](./kibana-plugin-core-server.routeconfigoptions.authrequired.md) | boolean | 'optional' | Defines authentication mode for a route: - true. A user has to have valid credentials to access a resource - false. A user can access a resource without any credentials. - 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid. Can be useful when we grant access to a resource but want to identify a user if possible.Defaults to true if an auth mechanism is registered. | | [body](./kibana-plugin-core-server.routeconfigoptions.body.md) | Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody | Additional body options [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md). | | [tags](./kibana-plugin-core-server.routeconfigoptions.tags.md) | readonly string[] | Additional metadata tag strings to attach to the route. | | [timeout](./kibana-plugin-core-server.routeconfigoptions.timeout.md) | {
payload?: Method extends 'get' | 'options' ? undefined : number;
idleSocket?: number;
} | Defines per-route timeouts. | diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 1cedddc1d1e5c..b0510bc414bf8 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -225,16 +225,15 @@ export class HttpServer { private getAuthOption( authRequired: RouteConfigOptions['authRequired'] = true - ): undefined | false | { mode: 'required' | 'optional' | 'try' } { + ): undefined | false | { mode: 'required' | 'try' } { if (this.authRegistered === false) return undefined; if (authRequired === true) { return { mode: 'required' }; } if (authRequired === 'optional') { - return { mode: 'optional' }; - } - if (authRequired === 'try') { + // we want to use HAPI `try` mode and not `optional` to not throw unauthorized errors when the user + // has invalid or expired credentials return { mode: 'try' }; } if (authRequired === false) { diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 33bdf28c6d901..6c11534df0d11 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -136,37 +136,6 @@ describe('http service', () => { await root.start(); await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); }); - - it('returns true if authenticated on a route with "try" auth', async () => { - const { http } = await root.setup(); - const { createRouter, auth, registerAuth } = http; - - registerAuth((req, res, toolkit) => toolkit.authenticated()); - const router = createRouter(''); - router.get( - { path: '/is-auth', validate: false, options: { authRequired: 'try' } }, - (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } }) - ); - - await root.start(); - await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: true }); - }); - - it('returns false if not authenticated on a route with "try" auth', async () => { - const { http } = await root.setup(); - const { createRouter, auth, registerAuth } = http; - - registerAuth((req, res, toolkit) => toolkit.notHandled()); - - const router = createRouter(''); - router.get( - { path: '/is-auth', validate: false, options: { authRequired: 'try' } }, - (context, req, res) => res.ok({ body: { isAuthenticated: auth.isAuthenticated(req) } }) - ); - - await root.start(); - await kbnTestServer.request.get(root, '/is-auth').expect(200, { isAuthenticated: false }); - }); }); describe('#get()', () => { it('returns authenticated status and allow associate auth state with request', async () => { diff --git a/src/core/server/http/integration_tests/http_auth.test.ts b/src/core/server/http/integration_tests/http_auth.test.ts index 2aa4d2796a6f2..0696deb9c07ae 100644 --- a/src/core/server/http/integration_tests/http_auth.test.ts +++ b/src/core/server/http/integration_tests/http_auth.test.ts @@ -146,46 +146,6 @@ describe('http auth', () => { await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); }); - it('blocks access when auth returns `unauthorized`', async () => { - const { http } = await root.setup(); - const { registerAuth, createRouter, auth } = http; - - registerAuth((req, res, toolkit) => res.unauthorized()); - - const router = createRouter(''); - registerRoute(router, auth, 'optional'); - - await root.start(); - await kbnTestServer.request.get(root, '/route').expect(401); - }); - }); - describe('when authRequired is `try`', () => { - it('allows authenticated access when auth returns `authenticated`', async () => { - const { http } = await root.setup(); - const { registerAuth, createRouter, auth } = http; - - registerAuth((req, res, toolkit) => toolkit.authenticated()); - - const router = createRouter(''); - registerRoute(router, auth, 'try'); - - await root.start(); - await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: true }); - }); - - it('allows anonymous access when auth returns `notHandled`', async () => { - const { http } = await root.setup(); - const { registerAuth, createRouter, auth } = http; - - registerAuth((req, res, toolkit) => toolkit.notHandled()); - - const router = createRouter(''); - registerRoute(router, auth, 'try'); - - await root.start(); - await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); - }); - it('allows anonymous access when auth returns `unauthorized`', async () => { const { http } = await root.setup(); const { registerAuth, createRouter, auth } = http; @@ -193,7 +153,7 @@ describe('http auth', () => { registerAuth((req, res, toolkit) => res.unauthorized()); const router = createRouter(''); - registerRoute(router, auth, 'try'); + registerRoute(router, auth, 'optional'); await root.start(); await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); @@ -234,16 +194,5 @@ describe('http auth', () => { await root.start(); await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); }); - - it('allow access to resources when `authRequired` is `try`', async () => { - const { http } = await root.setup(); - const { createRouter, auth } = http; - - const router = createRouter(''); - registerRoute(router, auth, 'try'); - - await root.start(); - await kbnTestServer.request.get(root, '/route').expect(200, { authenticated: false }); - }); }); }); diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 248b1e1278c4c..03324dc6c722f 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -114,19 +114,30 @@ describe('Options', () => { }); }); - it('User with invalid credentials cannot access a route', async () => { - const { server: innerServer, createRouter, registerAuth } = await server.setup(setupDeps); + it('User with invalid credentials can access a route', async () => { + const { server: innerServer, createRouter, registerAuth, auth } = await server.setup( + setupDeps + ); const router = createRouter('/'); registerAuth((req, res, toolkit) => res.unauthorized()); router.get( { path: '/', validate: false, options: { authRequired: 'optional' } }, - (context, req, res) => res.ok({ body: 'ok' }) + (context, req, res) => + res.ok({ + body: { + httpAuthIsAuthenticated: auth.isAuthenticated(req), + requestIsAuthenticated: req.auth.isAuthenticated, + }, + }) ); await server.start(); - await supertest(innerServer.listener).get('/').expect(401); + await supertest(innerServer.listener).get('/').expect(200, { + httpAuthIsAuthenticated: false, + requestIsAuthenticated: false, + }); }); it('does not redirect user and allows access to a resource', async () => { diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts index 758bfad874d90..167cf0747b4c1 100644 --- a/src/core/server/http/lifecycle/auth.ts +++ b/src/core/server/http/lifecycle/auth.ts @@ -123,7 +123,7 @@ export interface AuthToolkit { authenticated: (data?: AuthResultParams) => AuthResult; /** * User has no credentials. - * Allows user to access a resource when authRequired is 'optional' or 'try' + * Allows user to access a resource when authRequired is 'optional' * Rejects a request when authRequired: true * */ notHandled: () => AuthResult; diff --git a/src/core/server/http/router/route.ts b/src/core/server/http/router/route.ts index 879b48f7253a0..77b40ca5995bb 100644 --- a/src/core/server/http/router/route.ts +++ b/src/core/server/http/router/route.ts @@ -108,14 +108,12 @@ export interface RouteConfigOptions { * Defines authentication mode for a route: * - true. A user has to have valid credentials to access a resource * - false. A user can access a resource without any credentials. - * - 'optional'. A user can access a resource if has valid credentials or no credentials at all. + * - 'optional'. A user can access a resource, and will be authenticated if provided credentials are valid. * Can be useful when we grant access to a resource but want to identify a user if possible. - * - 'try'. A user can access a resource with valid, invalid or without any credentials. - * Users with valid credentials will be authenticated * * Defaults to `true` if an auth mechanism is registered. */ - authRequired?: boolean | 'optional' | 'try'; + authRequired?: boolean | 'optional'; /** * Defines xsrf protection requirements for a route: diff --git a/src/core/server/rendering/bootstrap/register_bootstrap_route.ts b/src/core/server/rendering/bootstrap/register_bootstrap_route.ts index 2c5274e89a221..5644b44f3508b 100644 --- a/src/core/server/rendering/bootstrap/register_bootstrap_route.ts +++ b/src/core/server/rendering/bootstrap/register_bootstrap_route.ts @@ -20,7 +20,7 @@ export const registerBootstrapRoute = ({ { path: '/bootstrap.js', options: { - authRequired: 'try', + authRequired: 'optional', tags: ['api'], }, validate: false, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 78b97d1c3f52e..a1a774e8721c8 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1966,7 +1966,7 @@ export interface RouteConfig { // @public export interface RouteConfigOptions { - authRequired?: boolean | 'optional' | 'try'; + authRequired?: boolean | 'optional'; body?: Method extends 'get' | 'options' ? undefined : RouteConfigOptionsBody; tags?: readonly string[]; timeout?: { From a1987445534be700e575e0b1831e47b63c71a9f4 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Wed, 10 Mar 2021 14:36:23 -0500 Subject: [PATCH 08/26] [App Search] Fixed 2 relevance tuning bugs (#94312) --- .../boost_item_content/boost_item_content.tsx | 8 ++--- .../functional_boost_form.test.tsx | 5 +-- .../functional_boost_form.tsx | 6 ++-- .../proximity_boost_form.test.tsx | 10 +++--- .../proximity_boost_form.tsx | 4 +-- .../value_boost_form.test.tsx | 6 ++-- .../boost_item_content/value_boost_form.tsx | 4 +-- .../relevance_tuning/boosts/boosts.test.tsx | 18 ++++++++++ .../relevance_tuning/boosts/boosts.tsx | 4 ++- .../components/relevance_tuning/constants.ts | 35 +++++++++++++++++++ .../relevance_tuning_logic.test.ts | 6 ++++ .../relevance_tuning_logic.ts | 16 +++------ .../components/relevance_tuning/types.ts | 30 +++++++++++----- 13 files changed, 112 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx index f83ec99acb1ac..7b9dd6b26cbb2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx @@ -13,7 +13,7 @@ import { EuiButton, EuiFormRow, EuiPanel, EuiRange, EuiSpacer } from '@elastic/e import { i18n } from '@kbn/i18n'; import { RelevanceTuningLogic } from '../..'; -import { Boost, BoostType } from '../../types'; +import { Boost, BoostType, FunctionalBoost, ProximityBoost, ValueBoost } from '../../types'; import { FunctionalBoostForm } from './functional_boost_form'; import { ProximityBoostForm } from './proximity_boost_form'; @@ -32,11 +32,11 @@ export const BoostItemContent: React.FC = ({ boost, index, name }) => { const getBoostForm = () => { switch (type) { case BoostType.Value: - return ; + return ; case BoostType.Functional: - return ; + return ; case BoostType.Proximity: - return ; + return ; } }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx index 11a224a71d7f8..feb4328e5adea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx @@ -13,12 +13,13 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiSelect } from '@elastic/eui'; -import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from '../../types'; +import { FunctionalBoost, BoostOperation, BoostType, FunctionalBoostFunction } from '../../types'; import { FunctionalBoostForm } from './functional_boost_form'; describe('FunctionalBoostForm', () => { - const boost: Boost = { + const boost: FunctionalBoost = { + value: undefined, factor: 2, type: 'functional' as BoostType, function: 'logarithmic' as FunctionalBoostFunction, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx index d677fe5cbc069..ebd826dcd27ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx @@ -19,7 +19,7 @@ import { FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP, } from '../../constants'; import { - Boost, + FunctionalBoost, BoostFunction, BoostOperation, BoostType, @@ -27,7 +27,7 @@ import { } from '../../types'; interface Props { - boost: Boost; + boost: FunctionalBoost; index: number; name: string; } @@ -39,7 +39,7 @@ const functionOptions = Object.values(FunctionalBoostFunction).map((boostFunctio const operationOptions = Object.values(BoostOperation).map((boostOperation) => ({ value: boostOperation, - text: BOOST_OPERATION_DISPLAY_MAP[boostOperation as BoostOperation], + text: BOOST_OPERATION_DISPLAY_MAP[boostOperation], })); export const FunctionalBoostForm: React.FC = ({ boost, index, name }) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx index 6abbcc3d98862..0ed914abb3ab5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx @@ -13,12 +13,14 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiFieldText, EuiSelect } from '@elastic/eui'; -import { Boost, BoostType, ProximityBoostFunction } from '../../types'; +import { ProximityBoost, BoostType, ProximityBoostFunction } from '../../types'; import { ProximityBoostForm } from './proximity_boost_form'; describe('ProximityBoostForm', () => { - const boost: Boost = { + const boost: ProximityBoost = { + value: undefined, + operation: undefined, factor: 2, type: 'proximity' as BoostType, function: 'linear' as ProximityBoostFunction, @@ -46,8 +48,8 @@ describe('ProximityBoostForm', () => { describe('various boost values', () => { const renderWithBoostValues = (boostValues: { - center?: Boost['center']; - function?: Boost['function']; + center?: ProximityBoost['center']; + function?: ProximityBoost['function']; }) => { return shallow( { - const boost: Boost = { + const boost: ValueBoost = { + operation: undefined, + function: undefined, factor: 2, type: 'value' as BoostType, value: ['bar', '', 'baz'], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx index 7fcd07d9a07aa..cd65f2842a5f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx @@ -20,10 +20,10 @@ import { import { i18n } from '@kbn/i18n'; import { RelevanceTuningLogic } from '../..'; -import { Boost } from '../../types'; +import { ValueBoost } from '../../types'; interface Props { - boost: Boost; + boost: ValueBoost; index: number; name: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx index 8a355b97e7b9f..9f6e194f92735 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx @@ -78,6 +78,24 @@ describe('Boosts', () => { expect(select.prop('options').map((o: any) => o.value)).toEqual(['add-boost', 'proximity']); }); + it('will not render functional option if "type" prop is "date"', () => { + const wrapper = shallow( + + ); + + const select = wrapper.find(EuiSuperSelect); + expect(select.prop('options').map((o: any) => o.value)).toEqual([ + 'add-boost', + 'proximity', + 'value', + ]); + }); + it('will add a boost of the selected type when a selection is made', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx index 4268e21110277..0aa1753938f46 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx @@ -13,7 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiSuperSelect } from '@ import { i18n } from '@kbn/i18n'; -import { GEOLOCATION, TEXT } from '../../../../shared/constants/field_types'; +import { GEOLOCATION, TEXT, DATE } from '../../../../shared/constants/field_types'; import { SchemaTypes } from '../../../../shared/types'; import { BoostIcon } from '../boost_icon'; @@ -70,6 +70,8 @@ const filterInvalidOptions = (value: BoostType, type: SchemaTypes) => { if (type === TEXT && [BoostType.Proximity, BoostType.Functional].includes(value)) return false; // Value and Functional boost types are not valid for geolocation fields if (type === GEOLOCATION && [BoostType.Functional, BoostType.Value].includes(value)) return false; + // Functional boosts are not valid for date fields + if (type === DATE && value === BoostType.Functional) return false; return true; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts index 8131a6a3a57c6..181ecad9e9990 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts @@ -10,8 +10,11 @@ import { i18n } from '@kbn/i18n'; import { BoostOperation, BoostType, + FunctionalBoost, FunctionalBoostFunction, + ProximityBoost, ProximityBoostFunction, + ValueBoost, } from './types'; export const FIELD_FILTER_CUTOFF = 10; @@ -77,6 +80,38 @@ export const BOOST_TYPE_TO_ICON_MAP = { [BoostType.Proximity]: 'tokenGeo', }; +const EMPTY_VALUE_BOOST: ValueBoost = { + type: BoostType.Value, + factor: 1, + newBoost: true, + function: undefined, + operation: undefined, +}; + +const EMPTY_FUNCTIONAL_BOOST: FunctionalBoost = { + value: undefined, + type: BoostType.Functional, + factor: 1, + newBoost: true, + function: FunctionalBoostFunction.Logarithmic, + operation: BoostOperation.Multiply, +}; + +const EMPTY_PROXIMITY_BOOST: ProximityBoost = { + value: undefined, + type: BoostType.Proximity, + factor: 1, + newBoost: true, + operation: undefined, + function: ProximityBoostFunction.Gaussian, +}; + +export const BOOST_TYPE_TO_EMPTY_BOOST = { + [BoostType.Value]: EMPTY_VALUE_BOOST, + [BoostType.Functional]: EMPTY_FUNCTIONAL_BOOST, + [BoostType.Proximity]: EMPTY_PROXIMITY_BOOST, +}; + export const ADD_DISPLAY = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.addOperationDropDownOptionLabel', { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index bcb8ad8a584a5..f0fe98f3f0a87 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -719,6 +719,9 @@ describe('RelevanceTuningLogic', () => { factor: 1, newBoost: true, type: BoostType.Functional, + function: 'logarithmic', + operation: 'multiply', + value: undefined, }, ], }, @@ -744,6 +747,9 @@ describe('RelevanceTuningLogic', () => { factor: 1, newBoost: true, type: BoostType.Functional, + function: 'logarithmic', + operation: 'multiply', + value: undefined, }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index 0d30296de285f..588b100416d10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -20,15 +20,9 @@ import { RESET_CONFIRMATION_MESSAGE, DELETE_SUCCESS_MESSAGE, DELETE_CONFIRMATION_MESSAGE, + BOOST_TYPE_TO_EMPTY_BOOST, } from './constants'; -import { - BaseBoost, - Boost, - BoostFunction, - BoostOperation, - BoostType, - SearchSettings, -} from './types'; +import { Boost, BoostFunction, BoostOperation, BoostType, SearchSettings } from './types'; import { filterIfTerm, parseBoostCenter, @@ -87,12 +81,12 @@ interface RelevanceTuningActions { updateBoostSelectOption( name: string, boostIndex: number, - optionType: keyof BaseBoost, + optionType: keyof Pick, value: BoostOperation | BoostFunction ): { name: string; boostIndex: number; - optionType: keyof BaseBoost; + optionType: keyof Pick; value: string; }; updateSearchValue(query: string): string; @@ -376,7 +370,7 @@ export const RelevanceTuningLogic = kea< addBoost: ({ name, type }) => { const { searchSettings } = values; const { boosts } = searchSettings; - const emptyBoost = { type, factor: 1, newBoost: true }; + const emptyBoost = BOOST_TYPE_TO_EMPTY_BOOST[type]; let boostArray; if (Array.isArray(boosts[name])) { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts index 16da5868da681..ec42df218878f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts @@ -29,26 +29,38 @@ export enum BoostOperation { Add = 'add', Multiply = 'multiply', } - -export interface BaseBoost { +export interface Boost { + type: BoostType; operation?: BoostOperation; function?: BoostFunction; + newBoost?: boolean; + center?: string | number; + factor: number; + value?: string[]; } // A boost that comes from the server, before we normalize it has a much looser schema -export interface RawBoost extends BaseBoost { - type: BoostType; - newBoost?: boolean; - center?: string | number; +export interface RawBoost extends Omit { value?: string | number | boolean | object | Array; - factor: number; } -// We normalize raw boosts to make them safer and easier to work with -export interface Boost extends RawBoost { +export interface ValueBoost extends Boost { value?: string[]; + operation: undefined; + function: undefined; } +export interface FunctionalBoost extends Boost { + value: undefined; + operation: BoostOperation; + function: FunctionalBoostFunction; +} + +export interface ProximityBoost extends Boost { + value: undefined; + operation: undefined; + function: ProximityBoostFunction; +} export interface SearchField { weight: number; } From a632f3f59ff87499a1aa98184e1357b22f2e4262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Wed, 10 Mar 2021 20:06:11 +0000 Subject: [PATCH 09/26] [ILM] Add support for frozen phase (#93068) --- .../edit_policy/edit_policy.helpers.tsx | 35 ++- .../edit_policy/features/frozen_phase.test.ts | 84 ++++++ .../features/node_allocation.test.ts | 52 ++-- .../edit_policy/features/rollover.test.ts | 10 + .../features/searchable_snapshots.test.ts | 6 +- .../client_integration/helpers/index.ts | 3 +- .../common/constants/data_tiers.ts | 8 + .../common/types/index.ts | 2 +- .../common/types/policies.ts | 26 +- .../public/application/constants/policy.ts | 1 + .../components/phase_icon/phase_icon.scss | 3 + .../phases/cold_phase/cold_phase.tsx | 37 +-- .../phases/delete_phase/delete_phase.tsx | 27 +- .../phases/frozen_phase/frozen_phase.tsx | 20 ++ .../components/phases/frozen_phase/index.ts | 8 + .../edit_policy/components/phases/index.ts | 2 + .../components/phases/phase/phase.tsx | 44 +-- .../components/data_tier_allocation.tsx | 46 ++- .../components/default_allocation_notice.tsx | 18 ++ .../components/default_allocation_warning.tsx | 13 + .../components/index.ts | 2 +- ...out.tsx => missing_cloud_tier_callout.tsx} | 26 +- .../components/no_node_attributes_warning.tsx | 9 + .../data_tier_allocation_field.tsx | 14 +- .../phases/shared_fields/freeze_field.tsx | 47 +++ .../components/phases/shared_fields/index.ts | 2 + .../shared_fields/index_priority_field.tsx | 2 +- .../phases/shared_fields/replicas_field.tsx | 2 +- .../searchable_snapshot_field.tsx | 114 ++++---- .../components/policy_json_flyout.tsx | 1 + .../timeline/timeline.container.tsx | 1 + .../components/timeline/timeline.scss | 8 + .../components/timeline/timeline.tsx | 27 +- .../sections/edit_policy/edit_policy.tsx | 15 +- .../form/components/enhanced_use_field.tsx | 4 + .../form/configuration_issues_context.tsx | 7 + .../sections/edit_policy/form/deserializer.ts | 7 +- .../edit_policy/form/form_errors_context.tsx | 2 + .../sections/edit_policy/form/index.ts | 2 +- .../form/phase_timings_context.tsx | 19 +- .../sections/edit_policy/form/schema.ts | 270 +++++++++--------- .../edit_policy/form/serializer/serializer.ts | 17 ++ .../sections/edit_policy/i18n_texts.ts | 20 +- .../lib/absolute_timing_to_relative_timing.ts | 10 +- .../application/sections/edit_policy/types.ts | 6 + .../routes/api/nodes/register_list_route.ts | 1 + .../api/policies/register_create_route.ts | 1 + .../translations/translations/ja-JP.json | 9 - .../translations/translations/zh-CN.json | 9 - .../index_lifecycle_management/fixtures.js | 10 +- 50 files changed, 756 insertions(+), 353 deletions(-) create mode 100644 x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/frozen_phase.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/index.ts rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/{missing_cold_tier_callout.tsx => missing_cloud_tier_callout.tsx} (70%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/freeze_field.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index b692d7fe69cd4..2b5319cafb1d4 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -208,8 +208,8 @@ export const setup = async (arg?: { }; }; - const setFreeze = createFormToggleAction('freezeSwitch'); - const freezeExists = () => exists('freezeSwitch'); + const createSetFreeze = (phase: Phases) => createFormToggleAction(`${phase}-freezeSwitch`); + const createFreezeExists = (phase: Phases) => () => exists(`${phase}-freezeSwitch`); const createReadonlyActions = (phase: Phases) => { const toggleSelector = `${phase}-readonlySwitch`; @@ -275,21 +275,21 @@ export const setup = async (arg?: { const dataTierSelector = `${controlsSelector}.dataTierSelect`; const nodeAttrsSelector = `${phase}-selectedNodeAttrs`; + const openNodeAttributesSection = async () => { + await act(async () => { + find(dataTierSelector).simulate('click'); + }); + component.update(); + }; + return { hasDataTierAllocationControls: () => exists(controlsSelector), - openNodeAttributesSection: async () => { - await act(async () => { - find(dataTierSelector).simulate('click'); - }); - component.update(); - }, + openNodeAttributesSection, hasNodeAttributesSelect: (): boolean => exists(nodeAttrsSelector), getNodeAttributesSelectOptions: () => find(nodeAttrsSelector).find('option'), setDataAllocation: async (value: DataTierAllocationType) => { - act(() => { - find(dataTierSelector).simulate('click'); - }); - component.update(); + await openNodeAttributesSection(); + await act(async () => { switch (value) { case 'node_roles': @@ -359,6 +359,7 @@ export const setup = async (arg?: { hasHotPhase: () => exists('ilmTimelineHotPhase'), hasWarmPhase: () => exists('ilmTimelineWarmPhase'), hasColdPhase: () => exists('ilmTimelineColdPhase'), + hasFrozenPhase: () => exists('ilmTimelineFrozenPhase'), hasDeletePhase: () => exists('ilmTimelineDeletePhase'), }, hot: { @@ -390,13 +391,19 @@ export const setup = async (arg?: { enable: enable('cold'), ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), - setFreeze, - freezeExists, + setFreeze: createSetFreeze('cold'), + freezeExists: createFreezeExists('cold'), hasErrorIndicator: () => exists('phaseErrorIndicator-cold'), ...createIndexPriorityActions('cold'), ...createSearchableSnapshotActions('cold'), ...createNodeAllocationActions('cold'), }, + frozen: { + enable: enable('frozen'), + ...createMinAgeActions('frozen'), + hasErrorIndicator: () => exists('phaseErrorIndicator-frozen'), + ...createSearchableSnapshotActions('frozen'), + }, delete: { isShown: () => exists('delete-phaseContent'), ...createToggleDeletePhaseActions(), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts new file mode 100644 index 0000000000000..3103a4198fc3b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/frozen_phase.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { licensingMock } from '../../../../../licensing/public/mocks'; +import { setupEnvironment } from '../../helpers/setup_environment'; +import { EditPolicyTestBed, setup } from '../edit_policy.helpers'; +import { getDefaultHotPhasePolicy } from '../constants'; + +describe(' frozen phase', () => { + let testBed: EditPolicyTestBed; + const { server, httpRequestsMockHelpers } = setupEnvironment(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + server.restore(); + }); + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: { data: ['node1'] }, + nodesByAttributes: { 'attribute:true': ['node1'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + httpRequestsMockHelpers.setNodesDetails('attribute:true', [ + { nodeId: 'testNodeId', stats: { name: 'testNodeName', host: 'testHost' } }, + ]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + + await act(async () => { + testBed = await setup(); + }); + + const { component } = testBed; + component.update(); + }); + + test('shows timing only when enabled', async () => { + const { actions, exists } = testBed; + + expect(exists('frozen-phase')).toBe(true); + expect(actions.frozen.hasMinAgeInput()).toBeFalsy(); + await actions.frozen.enable(true); + expect(actions.frozen.hasMinAgeInput()).toBeTruthy(); + }); + + describe('on non-enterprise license', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['my-repo'] }); + + await act(async () => { + testBed = await setup({ + appServicesContext: { + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); + }); + + const { component } = testBed; + component.update(); + }); + + test('should not be available', async () => { + const { exists } = testBed; + expect(exists('frozen-phase')).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts index 13e55a1f39e2c..832963827663d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/node_allocation.test.ts @@ -216,6 +216,7 @@ describe(' node allocation', () => { test('shows view node attributes link when attribute selected and shows flyout when clicked', async () => { const { actions, component } = testBed; + await actions.cold.enable(true); expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); @@ -250,23 +251,37 @@ describe(' node allocation', () => { expect(actions.cold.hasDefaultAllocationWarning()).toBeTruthy(); }); - test('shows default allocation notice when warm or hot tiers exists, but not cold tier', async () => { - httpRequestsMockHelpers.setListNodes({ - nodesByAttributes: {}, + [ + { + nodesByRoles: { data_hot: ['test'] }, + previousActiveRole: 'hot', + }, + { nodesByRoles: { data_hot: ['test'], data_warm: ['test'] }, - isUsingDeprecatedDataRoleConfig: false, - }); + previousActiveRole: 'warm', + }, + ].forEach(({ nodesByRoles, previousActiveRole }) => { + test(`shows default allocation notice when ${previousActiveRole} tiers exists, but not cold tier`, async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: {}, + nodesByRoles, + isUsingDeprecatedDataRoleConfig: false, + }); - await act(async () => { - testBed = await setup(); - }); - const { actions, component } = testBed; + await act(async () => { + testBed = await setup(); + }); + const { actions, component, find } = testBed; - component.update(); - await actions.cold.enable(true); + component.update(); + await actions.cold.enable(true); - expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy(); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + expect(actions.cold.hasDefaultAllocationNotice()).toBeTruthy(); + expect(find('defaultAllocationNotice').text()).toContain( + `This policy will move data in the cold phase to ${previousActiveRole} tier nodes` + ); + }); }); test(`doesn't show default allocation notice when node with "data" role exists`, async () => { @@ -366,7 +381,7 @@ describe(' node allocation', () => { expect(find('cloudDataTierCallout').exists()).toBeFalsy(); }); - test('shows cloud notice when cold tier nodes do not exist', async () => { + test(`shows cloud notice when cold tier nodes do not exist`, async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: {}, nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, @@ -375,13 +390,17 @@ describe(' node allocation', () => { await act(async () => { testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); }); - const { actions, component, exists } = testBed; + const { actions, component, exists, find } = testBed; component.update(); await actions.cold.enable(true); expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(exists('cloudMissingColdTierCallout')).toBeTruthy(); + expect(exists('cloudMissingTierCallout')).toBeTruthy(); + expect(find('cloudMissingTierCallout').text()).toContain( + `Edit your Elastic Cloud deployment to set up a cold tier` + ); + // Assert that other notices are not showing expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); @@ -480,6 +499,7 @@ describe(' node allocation', () => { const { find } = testBed; expect(find('warm-dataTierAllocationControls.dataTierSelect').text()).toBe('Custom'); }); + test('detecting use of the "off" allocation type', () => { const { find } = testBed; expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts index e2b67efbf588d..506ac4cece032 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/rollover.test.ts @@ -45,6 +45,7 @@ describe(' timeline', () => { const { actions } = testBed; expect(actions.hot.forceMergeFieldExists()).toBeTruthy(); }); + test('hides forcemerge when rollover is disabled', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); @@ -56,22 +57,26 @@ describe(' timeline', () => { const { actions } = testBed; expect(actions.hot.shrinkExists()).toBeTruthy(); }); + test('hides shrink input when rollover is disabled', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); await actions.hot.toggleRollover(false); expect(actions.hot.shrinkExists()).toBeFalsy(); }); + test('shows readonly input when rollover enabled', async () => { const { actions } = testBed; expect(actions.hot.readonlyExists()).toBeTruthy(); }); + test('hides readonly input when rollover is disabled', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); await actions.hot.toggleRollover(false); expect(actions.hot.readonlyExists()).toBeFalsy(); }); + test('hides and disables searchable snapshot field', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); @@ -86,12 +91,15 @@ describe(' timeline', () => { await actions.warm.enable(true); await actions.cold.enable(true); + await actions.frozen.enable(true); await actions.delete.enablePhase(); expect(actions.warm.hasRolloverTipOnMinAge()).toBeTruthy(); expect(actions.cold.hasRolloverTipOnMinAge()).toBeTruthy(); + expect(actions.frozen.hasRolloverTipOnMinAge()).toBeTruthy(); expect(actions.delete.hasRolloverTipOnMinAge()).toBeTruthy(); }); + test('hiding rollover tip on minimum age', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); @@ -99,10 +107,12 @@ describe(' timeline', () => { await actions.warm.enable(true); await actions.cold.enable(true); + await actions.frozen.enable(true); await actions.delete.enablePhase(); expect(actions.warm.hasRolloverTipOnMinAge()).toBeFalsy(); expect(actions.cold.hasRolloverTipOnMinAge()).toBeFalsy(); + expect(actions.frozen.hasRolloverTipOnMinAge()).toBeFalsy(); expect(actions.delete.hasRolloverTipOnMinAge()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts index ed678a6b217ae..44e03564cb89a 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/features/searchable_snapshots.test.ts @@ -94,6 +94,7 @@ describe(' searchable snapshots', () => { ).toBe(true); }); }); + describe('existing policy', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); @@ -124,6 +125,7 @@ describe(' searchable snapshots', () => { }); }); }); + describe('on non-enterprise license', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); @@ -145,11 +147,13 @@ describe(' searchable snapshots', () => { const { component } = testBed; component.update(); }); + test('disable setting searchable snapshots', async () => { const { actions } = testBed; - expect(actions.cold.searchableSnapshotsExists()).toBeFalsy(); expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); + expect(actions.cold.searchableSnapshotsExists()).toBeFalsy(); + expect(actions.frozen.searchableSnapshotsExists()).toBeFalsy(); await actions.cold.enable(true); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts index 6fe968edb9b05..56f5815633a1d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts @@ -21,5 +21,6 @@ export type TestSubjects = | 'policyTablePolicyNameLink' | 'policyTitle' | 'createPolicyButton' - | 'freezeSwitch' + | 'cold-freezeSwitch' + | 'frozen-freezeSwitch' | string; diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts b/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts index dcf8ce51a65ad..b00bb617b0be8 100644 --- a/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts +++ b/x-pack/plugins/index_lifecycle_management/common/constants/data_tiers.ts @@ -13,7 +13,15 @@ const WARM_PHASE_NODE_PREFERENCE: DataTierRole[] = ['data_warm', 'data_hot']; const COLD_PHASE_NODE_PREFERENCE: DataTierRole[] = ['data_cold', 'data_warm', 'data_hot']; +const FROZEN_PHASE_NODE_PREFERENCE: DataTierRole[] = [ + 'data_frozen', + 'data_cold', + 'data_warm', + 'data_hot', +]; + export const phaseToNodePreferenceMap: Record = Object.freeze({ warm: WARM_PHASE_NODE_PREFERENCE, cold: COLD_PHASE_NODE_PREFERENCE, + frozen: FROZEN_PHASE_NODE_PREFERENCE, }); diff --git a/x-pack/plugins/index_lifecycle_management/common/types/index.ts b/x-pack/plugins/index_lifecycle_management/common/types/index.ts index 3cd01f975d3b3..bc7e881a8c230 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/index.ts @@ -12,7 +12,7 @@ export * from './policies'; /** * These roles reflect how nodes are stratified into different data tiers. */ -export type DataTierRole = 'data_hot' | 'data_warm' | 'data_cold'; +export type DataTierRole = 'data_hot' | 'data_warm' | 'data_cold' | 'data_frozen'; /** * The "data_content" role can store all data the ES stack uses for feature diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 9f65286dc9b30..d3fec300d2d5f 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -7,7 +7,7 @@ import { Index as IndexInterface } from '../../../index_management/common/types'; -export type PhaseWithAllocation = 'warm' | 'cold'; +export type PhaseWithAllocation = 'warm' | 'cold' | 'frozen'; export interface SerializedPolicy { name: string; @@ -18,6 +18,7 @@ export interface Phases { hot?: SerializedHotPhase; warm?: SerializedWarmPhase; cold?: SerializedColdPhase; + frozen?: SerializedFrozenPhase; delete?: SerializedDeletePhase; } @@ -51,6 +52,8 @@ export interface SerializedActionWithAllocation { migrate?: MigrateAction; } +export type SearchableSnapshotStorage = 'full_copy' | 'shared_cache'; + export interface SearchableSnapshotAction { snapshot_repository: string; /** @@ -58,6 +61,12 @@ export interface SearchableSnapshotAction { * not suit the vast majority of cases. */ force_merge_index?: boolean; + /** + * This configuration lets the user create full or partial searchable snapshots. + * Full searchable snapshots store primary data locally and store replica data in the snapshot. + * Partial searchable snapshots store no data locally. + */ + storage?: SearchableSnapshotStorage; } export interface RolloverAction { @@ -111,6 +120,21 @@ export interface SerializedColdPhase extends SerializedPhase { }; } +export interface SerializedFrozenPhase extends SerializedPhase { + actions: { + freeze?: {}; + allocate?: AllocateAction; + set_priority?: { + priority: number | null; + }; + migrate?: MigrateAction; + /** + * Only available on enterprise license + */ + searchable_snapshot?: SearchableSnapshotAction; + }; +} + export interface SerializedDeletePhase extends SerializedPhase { actions: { wait_for_snapshot?: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts index 39c52391927a0..6e3134f9f2428 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/constants/policy.ts @@ -11,6 +11,7 @@ export const defaultIndexPriority = { hot: '100', warm: '50', cold: '0', + frozen: '0', }; export const defaultRolloverAction: RolloverAction = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss index 7c6a5aefdde6e..5bd6790dda572 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss @@ -23,6 +23,9 @@ &__inner--cold { fill: $euiColorVis1_behindText; } + &__inner--frozen { + fill: $euiColorVis4_behindText; + } &__inner--delete { fill: $euiColorDarkShade; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 1dbc30674eaa5..bc22516e6c996 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -6,20 +6,15 @@ */ import React, { FunctionComponent } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiTextColor } from '@elastic/eui'; - import { useConfigurationIssues } from '../../../form'; - -import { LearnMoreLink, ToggleFieldWithDescribedFormRow } from '../../'; - import { DataTierAllocationField, SearchableSnapshotField, IndexPriorityField, ReplicasField, + FreezeField, } from '../shared_fields'; import { Phase } from '../phase'; @@ -41,35 +36,7 @@ export const ColdPhase: FunctionComponent = () => { {/* Freeze section */} - {!isUsingSearchableSnapshotInHotPhase && ( - - - - } - description={ - - {' '} - - - } - fullWidth - titleSize="xs" - switchProps={{ - 'data-test-subj': 'freezeSwitch', - path: '_meta.cold.freezeEnabled', - }} - > -
- - )} + {!isUsingSearchableSnapshotInHotPhase && } {/* Data tier allocation section */} { ); return ( - } - className="ilmDeletePhase ilmPhase" - timelineIcon={} - > - - {i18nTexts.editPolicy.descriptions.delete} - + <> - - + } + className="ilmDeletePhase ilmPhase" + timelineIcon={} + > + + {i18nTexts.editPolicy.descriptions.delete} + + + + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/frozen_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/frozen_phase.tsx new file mode 100644 index 0000000000000..41cdb2a5f10fa --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/frozen_phase.tsx @@ -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 React, { FunctionComponent } from 'react'; + +import { SearchableSnapshotField } from '../shared_fields'; +import { Phase } from '../phase'; + +export const FrozenPhase: FunctionComponent = () => { + return ( + } + /> + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/index.ts new file mode 100644 index 0000000000000..e0f5ac9189adc --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/frozen_phase/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { FrozenPhase } from './frozen_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts index 62807d9499243..b248c15817548 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/index.ts @@ -11,6 +11,8 @@ export { WarmPhase } from './warm_phase'; export { ColdPhase } from './cold_phase'; +export { FrozenPhase } from './frozen_phase'; + export { DeletePhase } from './delete_phase'; export { Phase } from './phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx index 3a057f6204e24..040baf3625eb8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx @@ -23,16 +23,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { PhasesExceptDelete } from '../../../../../../../common/types'; import { ToggleField, useFormData } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; - import { FormInternal } from '../../../types'; - import { UseField } from '../../../form'; - -import { PhaseErrorIndicator } from './phase_error_indicator'; - import { MinAgeField } from '../shared_fields'; import { PhaseIcon } from '../../phase_icon'; import { PhaseFooter } from '../../phase_footer'; +import { PhaseErrorIndicator } from './phase_error_indicator'; + import './phase.scss'; interface Props { @@ -99,6 +96,7 @@ export const Phase: FunctionComponent = ({ children, topLevelSettings, ph actions={minAge} timelineIcon={} className={`ilmPhase ${enabled ? 'ilmPhase--enabled' : ''}`} + data-test-subj={`${phase}-phase`} > {i18nTexts.editPolicy.descriptions[phase]} @@ -115,20 +113,28 @@ export const Phase: FunctionComponent = ({ children, topLevelSettings, ph )} - - } - buttonClassName="ilmSettingsButton" - extraAction={} - > - - {children} - + {children ? ( + + } + buttonClassName="ilmSettingsButton" + extraAction={} + > + + {children} + + ) : ( + + + + + + )} )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx index b7a437d85add0..254063ac1a9ea 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/data_tier_allocation.tsx @@ -24,6 +24,17 @@ import './data_tier_allocation.scss'; type SelectOptions = EuiSuperSelectOption; +const customTexts = { + inputDisplay: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.customOption.input', + { defaultMessage: 'Custom' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.customOption.helpText', + { defaultMessage: 'Move data based on node attributes.' } + ), +}; + const i18nTexts = { allocationOptions: { warm: { @@ -47,16 +58,7 @@ const i18nTexts = { { defaultMessage: 'Do not move data in the warm phase.' } ), }, - custom: { - inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input', - { defaultMessage: 'Custom' } - ), - helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText', - { defaultMessage: 'Move data based on node attributes.' } - ), - }, + custom: customTexts, }, cold: { default: { @@ -79,16 +81,30 @@ const i18nTexts = { { defaultMessage: 'Do not move data in the cold phase.' } ), }, - custom: { + custom: customTexts, + }, + frozen: { + default: { + input: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.input', + { defaultMessage: 'Use frozen nodes (recommended)' } + ), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.defaultOption.helpText', + { defaultMessage: 'Move data to nodes in the frozen tier.' } + ), + }, + none: { inputDisplay: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input', - { defaultMessage: 'Custom' } + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.input', + { defaultMessage: 'Off' } ), helpText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText', - { defaultMessage: 'Move data based on node attributes.' } + 'xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.frozen.noneOption.helpText', + { defaultMessage: 'Do not move data in the frozen phase.' } ), }, + custom: customTexts, }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx index e43b750849774..b09d09d254085 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx @@ -21,6 +21,9 @@ const i18nTextsNodeRoleToDataTier: Record = { data_cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierColdLabel', { defaultMessage: 'cold', }), + data_frozen: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierFrozenLabel', { + defaultMessage: 'frozen', + }), }; const i18nTexts = { @@ -49,6 +52,21 @@ const i18nTexts = { values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] }, }), }, + frozen: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.frozen.title', + { defaultMessage: 'No nodes assigned to the frozen tier' } + ), + body: (nodeRole: DataTierRole) => + i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.frozen', + { + defaultMessage: + 'This policy will move data in the frozen phase to {tier} tier nodes instead.', + values: { tier: i18nTextsNodeRoleToDataTier[nodeRole] }, + } + ), + }, }, warning: { warm: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx index a194f3c07f900..649eb9f2fcb7f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx @@ -39,6 +39,19 @@ const i18nTexts = { } ), }, + frozen: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the frozen tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.frozenPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the frozen, cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', + } + ), + }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts index 938e0a850f933..dacec1df52e2e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts @@ -17,7 +17,7 @@ export { DefaultAllocationWarning } from './default_allocation_warning'; export { NoNodeAttributesWarning } from './no_node_attributes_warning'; -export { MissingColdTierCallout } from './missing_cold_tier_callout'; +export { MissingCloudTierCallout } from './missing_cloud_tier_callout'; export { CloudDataTierCallout } from './cloud_data_tier_callout'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx similarity index 70% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx index 21b8850e0b088..09d3135cde469 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cloud_tier_callout.tsx @@ -9,20 +9,23 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; -const i18nTexts = { - title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.title', { - defaultMessage: 'Create a cold tier', +const geti18nTexts = (tier: 'cold' | 'frozen') => ({ + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.title', { + defaultMessage: 'Create a {tier} tier', + values: { tier }, }), - body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.body', { - defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.body', { + defaultMessage: 'Edit your Elastic Cloud deployment to set up a {tier} tier.', + values: { tier }, }), linkText: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.linkToCloudDeploymentDescription', + 'xpack.indexLifecycleMgmt.editPolicy.cloudMissingTierCallout.linkToCloudDeploymentDescription', { defaultMessage: 'View cloud deployment' } ), -}; +}); interface Props { + phase: 'cold' | 'frozen'; linkToCloudDeployment?: string; } @@ -31,9 +34,14 @@ interface Props { * This may need to be change when we have autoscaling enabled on a cluster because nodes may not * yet exist, but will automatically be provisioned. */ -export const MissingColdTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => { +export const MissingCloudTierCallout: FunctionComponent = ({ + phase, + linkToCloudDeployment, +}) => { + const i18nTexts = geti18nTexts(phase); + return ( - + {i18nTexts.body}{' '} {Boolean(linkToCloudDeployment) && ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx index c4ca0c5e495e1..e6cadf7049962 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/no_node_attributes_warning.tsx @@ -33,6 +33,15 @@ const i18nTexts = { } ), }, + frozen: { + body: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozen.nodeAttributesMissingDescription', + { + defaultMessage: + 'Define custom node attributes in elasticsearch.yml to use attribute-based allocation.', + } + ), + }, }; export const NoNodeAttributesWarning: FunctionComponent<{ phase: PhaseWithAllocation }> = ({ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index 7a660e0379a8d..ef0e82063ce20 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -25,7 +25,7 @@ import { DefaultAllocationNotice, DefaultAllocationWarning, NoNodeAttributesWarning, - MissingColdTierCallout, + MissingCloudTierCallout, CloudDataTierCallout, LoadingError, } from './components'; @@ -71,17 +71,21 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr switch (allocationType) { case 'node_roles': /** - * We'll drive Cloud users to add a cold tier to their deployment if there are no nodes with the cold node role. + * We'll drive Cloud users to add a cold or frozen tier to their deployment if there are no nodes with that role. */ - if (isCloudEnabled && phase === 'cold' && !isUsingDeprecatedDataRoleConfig) { - const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length; + if ( + isCloudEnabled && + !isUsingDeprecatedDataRoleConfig && + (phase === 'cold' || phase === 'frozen') + ) { + const hasNoNodesWithNodeRole = !nodesByRoles[`data_${phase}` as const]?.length; if (hasDataNodeRoles && hasNoNodesWithNodeRole) { // Tell cloud users they can deploy nodes on cloud. return ( <> - + ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/freeze_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/freeze_field.tsx new file mode 100644 index 0000000000000..8db1829f03764 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/freeze_field.tsx @@ -0,0 +1,47 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiTextColor } from '@elastic/eui'; + +import { LearnMoreLink, ToggleFieldWithDescribedFormRow } from '../../'; + +interface Props { + phase: 'cold' | 'frozen'; +} + +export const FreezeField: FunctionComponent = ({ phase }) => { + return ( + + + + } + description={ + + {' '} + + + } + fullWidth + titleSize="xs" + switchProps={{ + 'data-test-subj': `${phase}-freezeSwitch`, + path: `_meta.${phase}.freezeEnabled`, + }} + > +
+ + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts index 220f0bd8e941a..91faf5c66df81 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index.ts @@ -22,3 +22,5 @@ export { ReadonlyField } from './readonly_field'; export { ReplicasField } from './replicas_field'; export { IndexPriorityField } from './index_priority_field'; + +export { FreezeField } from './freeze_field'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx index 507a99c4754b8..47d2aa6ba92df 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/index_priority_field.tsx @@ -18,7 +18,7 @@ import { UseField } from '../../../form'; import { LearnMoreLink, DescribedFormRow } from '../..'; interface Props { - phase: 'hot' | 'warm' | 'cold'; + phase: 'hot' | 'warm' | 'cold' | 'frozen'; } export const IndexPriorityField: FunctionComponent = ({ phase }) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx index 4f005eb4fd201..ea05e401822ce 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/replicas_field.tsx @@ -16,7 +16,7 @@ import { UseField } from '../../../form'; import { DescribedFormRow } from '../../described_form_row'; interface Props { - phase: 'warm' | 'cold'; + phase: 'warm' | 'cold' | 'frozen'; } export const ReplicasField: FunctionComponent = ({ phase }) => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 3fc7064575555..816e1aaec31d7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -17,28 +17,18 @@ import { EuiLink, } from '@elastic/eui'; -import { - ComboBoxField, - useKibana, - fieldValidators, - useFormData, -} from '../../../../../../../shared_imports'; +import { ComboBoxField, useKibana, useFormData } from '../../../../../../../shared_imports'; import { useEditPolicyContext } from '../../../../edit_policy_context'; -import { useConfigurationIssues, UseField } from '../../../../form'; - -import { i18nTexts } from '../../../../i18n_texts'; - +import { useConfigurationIssues, UseField, searchableSnapshotFields } from '../../../../form'; import { FieldLoadingError, DescribedFormRow, LearnMoreLink } from '../../../'; - import { SearchableSnapshotDataProvider } from './searchable_snapshot_data_provider'; import './_searchable_snapshot_field.scss'; -const { emptyField } = fieldValidators; - export interface Props { - phase: 'hot' | 'cold'; + phase: 'hot' | 'cold' | 'frozen'; + canBeDisabled?: boolean; } /** @@ -47,29 +37,62 @@ export interface Props { */ const CLOUD_DEFAULT_REPO = 'found-snapshots'; -export const SearchableSnapshotField: FunctionComponent = ({ phase }) => { +const geti18nTexts = (phase: Props['phase']) => ({ + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', { + defaultMessage: 'Searchable snapshot', + }), + description: + phase === 'frozen' ? ( + + ), + }} + /> + ) : ( + , + }} + /> + ), +}); + +export const SearchableSnapshotField: FunctionComponent = ({ + phase, + canBeDisabled = true, +}) => { const { services: { cloud }, } = useKibana(); const { getUrlForApp, policy, license, isNewPolicy } = useEditPolicyContext(); const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); - const searchableSnapshotPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; + const searchableSnapshotRepoPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; - const [formData] = useFormData({ watch: searchableSnapshotPath }); - const searchableSnapshotRepo = get(formData, searchableSnapshotPath); + const [formData] = useFormData({ watch: searchableSnapshotRepoPath }); + const searchableSnapshotRepo = get(formData, searchableSnapshotRepoPath); const isColdPhase = phase === 'cold'; + const isFrozenPhase = phase === 'frozen'; + const isColdOrFrozenPhase = isColdPhase || isFrozenPhase; const isDisabledDueToLicense = !license.canUseSearchableSnapshot(); const [isFieldToggleChecked, setIsFieldToggleChecked] = useState(() => Boolean( - // New policy on cloud should have searchable snapshot on in cold phase - (isColdPhase && isNewPolicy && cloud?.isCloudEnabled) || + // New policy on cloud should have searchable snapshot on in cold and frozen phase + (isColdOrFrozenPhase && isNewPolicy && cloud?.isCloudEnabled) || policy.phases[phase]?.actions?.searchable_snapshot?.snapshot_repository ) ); + const i18nTexts = geti18nTexts(phase); + useEffect(() => { if (isDisabledDueToLicense) { setIsFieldToggleChecked(false); @@ -180,17 +203,10 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) =>
config={{ - label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, + ...searchableSnapshotFields.snapshot_repository, defaultValue: cloud?.isCloudEnabled ? CLOUD_DEFAULT_REPO : undefined, - validations: [ - { - validator: emptyField( - i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired - ), - }, - ], }} - path={searchableSnapshotPath} + path={searchableSnapshotRepoPath} > {(field) => { const singleSelectionArray: [selectedSnapshot?: string] = field.value @@ -289,34 +305,24 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => return ( - {i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle', { - defaultMessage: 'Searchable snapshot', - })} - + switchProps={ + canBeDisabled + ? { + checked: isFieldToggleChecked, + disabled: isDisabledDueToLicense, + onChange: setIsFieldToggleChecked, + 'data-test-subj': 'searchableSnapshotToggle', + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotsToggleLabel', + { defaultMessage: 'Create searchable snapshot' } + ), + } + : undefined } + title={

{i18nTexts.title}

} description={ <> - - , - }} - /> - + {i18nTexts.description} } fieldNotices={renderInfoCallout()} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx index 2f5d00082cc8a..93547fdebffe5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -42,6 +42,7 @@ const prettifyFormJson = (policy: SerializedPolicy): SerializedPolicy => ({ hot: policy.phases.hot, warm: policy.phases.warm, cold: policy.phases.cold, + frozen: policy.phases.frozen, delete: policy.phases.delete, }, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx index c2aa011f582c7..88d9d2de03d89 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.container.tsx @@ -26,6 +26,7 @@ export const Timeline: FunctionComponent = () => { hotPhaseMinAge={timings.hot.min_age} warmPhaseMinAge={timings.warm?.min_age} coldPhaseMinAge={timings.cold?.min_age} + frozenPhaseMinAge={timings.frozen?.min_age} deletePhaseMinAge={timings.delete?.min_age} isUsingRollover={isUsingRollover} hasDeletePhase={Boolean(formData._meta?.delete?.enabled)} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss index de49e665ed933..983ef0ab20f69 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss @@ -80,4 +80,12 @@ $ilmTimelineBarHeight: $euiSizeS; background-color: $euiColorVis1; } } + + &__frozenPhase { + width: var(--ilm-timeline-frozen-phase-width); + + &__colorBar { + background-color: $euiColorVis4; + } + } } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx index c996c45171d2f..8a0028dcb8b19 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx @@ -62,6 +62,9 @@ const i18nTexts = { coldPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.coldPhaseSectionTitle', { defaultMessage: 'Cold phase', }), + frozenPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.frozenPhaseSectionTitle', { + defaultMessage: 'Frozen phase', + }), deleteIcon: { toolTipContent: i18n.translate('xpack.indexLifecycleMgmt.timeline.deleteIconToolTipContent', { defaultMessage: 'Policy deletes the index after lifecycle phases complete.', @@ -84,12 +87,17 @@ const calculateWidths = (inputs: PhaseAgeInMilliseconds) => { inputs.phases.cold != null ? msTimeToOverallPercent(inputs.phases.cold, inputs.total) + SCORE_BUFFER_AMOUNT : 0; + const frozenScore = + inputs.phases.frozen != null + ? msTimeToOverallPercent(inputs.phases.frozen, inputs.total) + SCORE_BUFFER_AMOUNT + : 0; - const totalScore = hotScore + warmScore + coldScore; + const totalScore = hotScore + warmScore + coldScore + frozenScore; return { hot: `${toPercent(hotScore, totalScore)}%`, warm: `${toPercent(warmScore, totalScore)}%`, cold: `${toPercent(coldScore, totalScore)}%`, + frozen: `${toPercent(frozenScore, totalScore)}%`, }; }; @@ -102,6 +110,7 @@ interface Props { isUsingRollover: boolean; warmPhaseMinAge?: string; coldPhaseMinAge?: string; + frozenPhaseMinAge?: string; deletePhaseMinAge?: string; } @@ -115,6 +124,9 @@ export const Timeline: FunctionComponent = memo( hot: { min_age: phasesMinAge.hotPhaseMinAge }, warm: phasesMinAge.warmPhaseMinAge ? { min_age: phasesMinAge.warmPhaseMinAge } : undefined, cold: phasesMinAge.coldPhaseMinAge ? { min_age: phasesMinAge.coldPhaseMinAge } : undefined, + frozen: phasesMinAge.frozenPhaseMinAge + ? { min_age: phasesMinAge.frozenPhaseMinAge } + : undefined, delete: phasesMinAge.deletePhaseMinAge ? { min_age: phasesMinAge.deletePhaseMinAge } : undefined, @@ -157,6 +169,7 @@ export const Timeline: FunctionComponent = memo( el.style.setProperty('--ilm-timeline-hot-phase-width', widths.hot); el.style.setProperty('--ilm-timeline-warm-phase-width', widths.warm ?? null); el.style.setProperty('--ilm-timeline-cold-phase-width', widths.cold ?? null); + el.style.setProperty('--ilm-timeline-frozen-phase-width', widths.frozen ?? null); } }} > @@ -198,6 +211,18 @@ export const Timeline: FunctionComponent = memo( />
)} + {exists(phaseAgeInMilliseconds.phases.frozen) && ( +
+
+ +
+ )}
{hasDeletePhase && ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index befb8faf51aa1..ed165e8638843 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -43,6 +43,7 @@ import { ColdPhase, DeletePhase, HotPhase, + FrozenPhase, PolicyJsonFlyout, WarmPhase, Timeline, @@ -72,6 +73,7 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { policy: currentPolicy, existingPolicies, policyName, + license, } = useEditPolicyContext(); const serializer = useMemo(() => { @@ -80,6 +82,7 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { const [saveAsNew, setSaveAsNew] = useState(false); const originalPolicyName: string = isNewPolicy ? '' : policyName!; + const isAllowedByLicense = license.canUseSearchableSnapshot(); const { form } = useForm({ schema, @@ -243,13 +246,21 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { - - + {isAllowedByLicense && ( + <> + + + + )} + + {/* We can't add the here as it breaks the layout + and makes the connecting line go further that it needs to. + There is an issue in EUI to fix this (https://github.com/elastic/eui/issues/4492) */}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx index 7210dc6b7ce2b..34999368ffab2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/enhanced_use_field.tsx @@ -23,6 +23,7 @@ const isXPhaseField = (phase: keyof Phases) => (fieldPath: string): boolean => const isHotPhaseField = isXPhaseField('hot'); const isWarmPhaseField = isXPhaseField('warm'); const isColdPhaseField = isXPhaseField('cold'); +const isFrozenPhaseField = isXPhaseField('frozen'); const isDeletePhaseField = isXPhaseField('delete'); const determineFieldPhase = (fieldPath: string): keyof Phases | 'other' => { @@ -35,6 +36,9 @@ const determineFieldPhase = (fieldPath: string): keyof Phases | 'other' => { if (isColdPhaseField(fieldPath)) { return 'cold'; } + if (isFrozenPhaseField(fieldPath)) { + return 'frozen'; + } if (isDeletePhaseField(fieldPath)) { return 'delete'; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx index cc021f101cfb5..c2e55f7aa6e61 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/configuration_issues_context.tsx @@ -25,6 +25,7 @@ export interface ConfigurationIssues { * See https://github.com/elastic/elasticsearch/blob/master/docs/reference/ilm/actions/ilm-searchable-snapshot.asciidoc. */ isUsingSearchableSnapshotInHotPhase: boolean; + isUsingSearchableSnapshotInColdPhase: boolean; } const ConfigurationIssuesContext = createContext(null as any); @@ -32,10 +33,14 @@ const ConfigurationIssuesContext = createContext(null as an const pathToHotPhaseSearchableSnapshot = 'phases.hot.actions.searchable_snapshot.snapshot_repository'; +const pathToColdPhaseSearchableSnapshot = + 'phases.cold.actions.searchable_snapshot.snapshot_repository'; + export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => { const [formData] = useFormData({ watch: [ pathToHotPhaseSearchableSnapshot, + pathToColdPhaseSearchableSnapshot, isUsingCustomRolloverPath, isUsingDefaultRolloverPath, ], @@ -50,6 +55,8 @@ export const ConfigurationIssuesProvider: FunctionComponent = ({ children }) => isUsingRollover: isUsingDefaultRollover === false ? isUsingCustomRollover : true, isUsingSearchableSnapshotInHotPhase: get(formData, pathToHotPhaseSearchableSnapshot) != null, + isUsingSearchableSnapshotInColdPhase: + get(formData, pathToColdPhaseSearchableSnapshot) != null, }} > {children} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index 3e70cbb533653..227f135ca7b72 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -17,7 +17,7 @@ import { FormInternal } from '../types'; export const deserializer = (policy: SerializedPolicy): FormInternal => { const { - phases: { hot, warm, cold, delete: deletePhase }, + phases: { hot, warm, cold, frozen, delete: deletePhase }, } = policy; const _meta: FormInternal['_meta'] = { @@ -41,6 +41,11 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { dataTierAllocationType: determineDataTierAllocationType(cold?.actions), freezeEnabled: Boolean(cold?.actions?.freeze), }, + frozen: { + enabled: Boolean(frozen), + dataTierAllocationType: determineDataTierAllocationType(frozen?.actions), + freezeEnabled: Boolean(frozen?.actions?.freeze), + }, delete: { enabled: Boolean(deletePhase), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx index 9877a2ea9449c..b4aab0ffdea60 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/form_errors_context.tsx @@ -26,6 +26,7 @@ interface Errors { hot: ErrorGroup; warm: ErrorGroup; cold: ErrorGroup; + frozen: ErrorGroup; delete: ErrorGroup; /** * Errors that are not specific to a phase should go here. @@ -46,6 +47,7 @@ const createEmptyErrors = (): Errors => ({ hot: {}, warm: {}, cold: {}, + frozen: {}, delete: {}, other: {}, }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts index 734a12a72bd30..6deb4d7fd4711 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts @@ -9,7 +9,7 @@ export { deserializer } from './deserializer'; export { createSerializer } from './serializer'; -export { schema } from './schema'; +export { schema, searchableSnapshotFields } from './schema'; export * from './validations'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx index 92cc8eeead91a..98ffb7e2dd7af 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx @@ -23,19 +23,24 @@ const getPhaseTimingConfiguration = ( hot: PhaseTimingConfiguration; warm: PhaseTimingConfiguration; cold: PhaseTimingConfiguration; + frozen: PhaseTimingConfiguration; } => { const isWarmPhaseEnabled = formData?._meta?.warm?.enabled; const isColdPhaseEnabled = formData?._meta?.cold?.enabled; + const isFrozenPhaseEnabled = formData?._meta?.frozen?.enabled; + return { - hot: { isFinalDataPhase: !isWarmPhaseEnabled && !isColdPhaseEnabled }, - warm: { isFinalDataPhase: isWarmPhaseEnabled && !isColdPhaseEnabled }, - cold: { isFinalDataPhase: isColdPhaseEnabled }, + hot: { isFinalDataPhase: !isWarmPhaseEnabled && !isColdPhaseEnabled && !isFrozenPhaseEnabled }, + warm: { isFinalDataPhase: isWarmPhaseEnabled && !isColdPhaseEnabled && !isFrozenPhaseEnabled }, + cold: { isFinalDataPhase: isColdPhaseEnabled && !isFrozenPhaseEnabled }, + frozen: { isFinalDataPhase: isFrozenPhaseEnabled }, }; }; export interface PhaseTimings { hot: PhaseTimingConfiguration; warm: PhaseTimingConfiguration; cold: PhaseTimingConfiguration; + frozen: PhaseTimingConfiguration; isDeletePhaseEnabled: boolean; setDeletePhaseEnabled: (enabled: boolean) => void; } @@ -44,7 +49,12 @@ const PhaseTimingsContext = createContext(null as any); export const PhaseTimingsProvider: FunctionComponent = ({ children }) => { const [formData] = useFormData({ - watch: ['_meta.warm.enabled', '_meta.cold.enabled', '_meta.delete.enabled'], + watch: [ + '_meta.warm.enabled', + '_meta.cold.enabled', + '_meta.frozen.enabled', + '_meta.delete.enabled', + ], }); return ( @@ -65,6 +75,7 @@ export const PhaseTimingsProvider: FunctionComponent = ({ children }) => { ); }; + export const usePhaseTimings = () => { const ctx = useContext(PhaseTimingsContext); if (!ctx) throw new Error('Cannot use phase timings outside of phase timings context'); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 65fc82b7ccc68..5861c7b320de1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -30,6 +30,90 @@ const serializers = { stringToNumber: (v: string): any => (v != null ? parseInt(v, 10) : undefined), }; +const maxNumSegmentsField = { + label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError', + { defaultMessage: 'A value for number of segments is required.' } + ) + ), + }, + { + validator: ifExistsNumberGreaterThanZero, + }, + ], + serializer: serializers.stringToNumber, +}; + +export const searchableSnapshotFields = { + snapshot_repository: { + label: i18nTexts.editPolicy.searchableSnapshotsRepoFieldLabel, + validations: [ + { validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) }, + ], + }, + storage: { + label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageLabel', { + defaultMessage: 'Storage', + }), + helpText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshot.storageHelpText', + { + defaultMessage: + "Type of snapshot mounted for the searchable snapshot. This is an advanced option. Only change it if you know what you're doing.", + } + ), + }, +}; + +const numberOfReplicasField = { + label: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.numberOfReplicasLabel', { + defaultMessage: 'Number of replicas', + }), + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { + validator: ifExistsNumberNonNegative, + }, + ], + serializer: serializers.stringToNumber, +}; + +const numberOfShardsField = { + label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', { + defaultMessage: 'Number of primary shards', + }), + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { + validator: numberGreaterThanField({ + message: i18nTexts.editPolicy.errors.numberGreatThan0Required, + than: 0, + }), + }, + ], + serializer: serializers.stringToNumber, +}; + +const getPriorityField = (phase: 'hot' | 'warm' | 'cold' | 'frozen') => ({ + defaultValue: defaultIndexPriority[phase] as any, + label: i18nTexts.editPolicy.indexPriorityFieldLabel, + validations: [ + { + validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), + }, + { validator: ifExistsNumberNonNegative }, + ], + serializer: serializers.stringToNumber, +}); + export const schema: FormSchema = { _meta: { hot: { @@ -110,6 +194,30 @@ export const schema: FormSchema = { label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel, }, }, + frozen: { + enabled: { + defaultValue: false, + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.activateFrozenPhaseSwitchLabel', + { defaultMessage: 'Activate frozen phase' } + ), + }, + freezeEnabled: { + defaultValue: false, + label: i18n.translate('xpack.indexLifecycleMgmt.frozePhase.freezeIndexLabel', { + defaultMessage: 'Freeze index', + }), + }, + minAgeUnit: { + defaultValue: 'd', + }, + dataTierAllocationType: { + label: i18nTexts.editPolicy.allocationTypeOptionsFieldLabel, + }, + allocationNodeAttribute: { + label: i18nTexts.editPolicy.allocationNodeAttributeFieldLabel, + }, + }, delete: { enabled: { defaultValue: false, @@ -172,55 +280,13 @@ export const schema: FormSchema = { }, }, forcemerge: { - max_num_segments: { - label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel, - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError', - { defaultMessage: 'A value for number of segments is required.' } - ) - ), - }, - { - validator: ifExistsNumberGreaterThanZero, - }, - ], - serializer: serializers.stringToNumber, - }, + max_num_segments: maxNumSegmentsField, }, shrink: { - number_of_shards: { - label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', { - defaultMessage: 'Number of primary shards', - }), - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { - validator: numberGreaterThanField({ - message: i18nTexts.editPolicy.errors.numberGreatThan0Required, - than: 0, - }), - }, - ], - serializer: serializers.stringToNumber, - }, + number_of_shards: numberOfShardsField, }, set_priority: { - priority: { - defaultValue: defaultIndexPriority.hot as any, - label: i18nTexts.editPolicy.indexPriorityFieldLabel, - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { validator: ifExistsNumberNonNegative }, - ], - serializer: serializers.stringToNumber, - }, + priority: getPriorityField('hot'), }, }, }, @@ -235,71 +301,16 @@ export const schema: FormSchema = { }, actions: { allocate: { - number_of_replicas: { - label: i18n.translate('xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel', { - defaultMessage: 'Number of replicas', - }), - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { - validator: ifExistsNumberNonNegative, - }, - ], - serializer: serializers.stringToNumber, - }, + number_of_replicas: numberOfReplicasField, }, shrink: { - number_of_shards: { - label: i18n.translate('xpack.indexLifecycleMgmt.shrink.numberOfPrimaryShardsLabel', { - defaultMessage: 'Number of primary shards', - }), - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { - validator: numberGreaterThanField({ - message: i18nTexts.editPolicy.errors.numberGreatThan0Required, - than: 0, - }), - }, - ], - serializer: serializers.stringToNumber, - }, + number_of_shards: numberOfShardsField, }, forcemerge: { - max_num_segments: { - label: i18nTexts.editPolicy.maxNumSegmentsFieldLabel, - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.forcemerge.numberOfSegmentsRequiredError', - { defaultMessage: 'A value for number of segments is required.' } - ) - ), - }, - { - validator: ifExistsNumberGreaterThanZero, - }, - ], - serializer: serializers.stringToNumber, - }, + max_num_segments: maxNumSegmentsField, }, set_priority: { - priority: { - defaultValue: defaultIndexPriority.warm as any, - label: i18nTexts.editPolicy.indexPriorityFieldLabel, - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { validator: ifExistsNumberNonNegative }, - ], - serializer: serializers.stringToNumber, - }, + priority: getPriorityField('warm'), }, }, }, @@ -314,42 +325,31 @@ export const schema: FormSchema = { }, actions: { allocate: { - number_of_replicas: { - label: i18n.translate('xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel', { - defaultMessage: 'Number of replicas', - }), - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { - validator: ifExistsNumberNonNegative, - }, - ], - serializer: serializers.stringToNumber, - }, + number_of_replicas: numberOfReplicasField, }, set_priority: { - priority: { - defaultValue: defaultIndexPriority.cold as any, - label: i18nTexts.editPolicy.indexPriorityFieldLabel, - validations: [ - { - validator: emptyField(i18nTexts.editPolicy.errors.numberRequired), - }, - { validator: ifExistsNumberNonNegative }, - ], - serializer: serializers.stringToNumber, - }, + priority: getPriorityField('cold'), }, - searchable_snapshot: { - snapshot_repository: { - label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, - validations: [ - { validator: emptyField(i18nTexts.editPolicy.errors.searchableSnapshotRepoRequired) }, - ], + searchable_snapshot: searchableSnapshotFields, + }, + }, + frozen: { + min_age: { + defaultValue: '0', + validations: [ + { + validator: minAgeValidator, }, + ], + }, + actions: { + allocate: { + number_of_replicas: numberOfReplicasField, + }, + set_priority: { + priority: getPriorityField('frozen'), }, + searchable_snapshot: searchableSnapshotFields, }, }, delete: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts index 746ba4eeb801f..b21545ce1739c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -241,6 +241,23 @@ export const createSerializer = (originalPolicy?: SerializedPolicy) => ( delete draft.phases.cold; } + /** + * FROZEN PHASE SERIALIZATION + */ + if (_meta.frozen.enabled) { + draft.phases.frozen!.actions = draft.phases.frozen?.actions ?? {}; + const frozenPhase = draft.phases.frozen!; + + /** + * FROZEN PHASE SEARCHABLE SNAPSHOT + */ + if (!updatedPolicy.phases.frozen?.actions?.searchable_snapshot) { + delete frozenPhase.actions.searchable_snapshot; + } + } else { + delete draft.phases.frozen; + } + /** * DELETE PHASE SERIALIZATION */ diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 1d75fb5031216..47585fba38768 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -77,12 +77,18 @@ export const i18nTexts = { defaultMessage: 'Select a node attribute', } ), - searchableSnapshotsFieldLabel: i18n.translate( - 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel', + searchableSnapshotsRepoFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoFieldLabel', { defaultMessage: 'Searchable snapshot repository', } ), + searchableSnapshotsStorageFieldLabel: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotStorageFieldLabel', + { + defaultMessage: 'Searchable snapshot storage', + } + ), errors: { numberRequired: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.errors.numberRequiredErrorMessage', @@ -188,6 +194,9 @@ export const i18nTexts = { cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle', { defaultMessage: 'Cold phase', }), + frozen: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.frozenPhase.frozenPhaseTitle', { + defaultMessage: 'Frozen phase', + }), delete: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseTitle', { defaultMessage: 'Delete phase', }), @@ -205,6 +214,13 @@ export const i18nTexts = { defaultMessage: 'Move data to the cold tier, which is optimized for cost savings over search performance. Data is normally read-only in the cold phase.', }), + frozen: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.frozenPhase.frozenPhaseDescription', + { + defaultMessage: + 'Archive data as searchable snapshots in the frozen tier. The frozen tier is optimized for maximum cost savings. Data in the frozen tier is rarely accessed and never updated.', + } + ), delete: i18n.translate( 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescription', { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts index 2974a88c22343..5d71bc057966e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -28,11 +28,11 @@ import { FormInternal } from '../types'; /* -===- Private functions and types -===- */ -type MinAgePhase = 'warm' | 'cold' | 'delete'; +type MinAgePhase = 'warm' | 'cold' | 'frozen' | 'delete'; type Phase = 'hot' | MinAgePhase; -const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete']; +const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'frozen', 'delete']; const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ min_age: formData.phases?.[phase]?.min_age @@ -69,6 +69,9 @@ export interface AbsoluteTimings { cold?: { min_age: string; }; + frozen?: { + min_age: string; + }; delete?: { min_age: string; }; @@ -80,6 +83,7 @@ export interface PhaseAgeInMilliseconds { hot: number; warm?: number; cold?: number; + frozen?: number; }; } @@ -92,6 +96,7 @@ export const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimin hot: { min_age: undefined }, warm: _meta.warm.enabled ? getMinAge('warm', formData) : undefined, cold: _meta.cold.enabled ? getMinAge('cold', formData) : undefined, + frozen: _meta.frozen?.enabled ? getMinAge('frozen', formData) : undefined, delete: _meta.delete.enabled ? getMinAge('delete', formData) : undefined, }; }; @@ -139,6 +144,7 @@ export const calculateRelativeFromAbsoluteMilliseconds = ( hot: 0, warm: inputs.warm ? 0 : undefined, cold: inputs.cold ? 0 : undefined, + frozen: inputs.frozen ? 0 : undefined, }, } ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index 7aabf5d48e4fd..4330cde378b6d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -53,6 +53,11 @@ interface ColdPhaseMetaFields extends DataAllocationMetaFields, MinAgeField { freezeEnabled: boolean; } +interface FrozenPhaseMetaFields extends DataAllocationMetaFields, MinAgeField { + enabled: boolean; + freezeEnabled: boolean; +} + interface DeletePhaseMetaFields extends MinAgeField { enabled: boolean; } @@ -69,6 +74,7 @@ export interface FormInternal extends SerializedPolicy { hot: HotPhaseMetaFields; warm: WarmPhaseMetaFields; cold: ColdPhaseMetaFields; + frozen: FrozenPhaseMetaFields; delete: DeletePhaseMetaFields; }; } diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts index b46b77f2776c9..accd8993abc62 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/nodes/register_list_route.ts @@ -75,6 +75,7 @@ export function registerListRoute({ 'ml.enabled', 'ml.machine_memory', 'ml.max_open_jobs', + 'ml.max_jvm_size', // Used by ML to identify nodes that have transform enabled: // https://github.com/elastic/elasticsearch/pull/52712/files#diff-225cc2c1291b4c60a8c3412a619094e1R147 'transform.node', diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts index 1cca7d29874d6..7a4795f8e370b 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_create_route.ts @@ -37,6 +37,7 @@ const bodySchema = schema.object({ hot: schema.any(), warm: schema.maybe(schema.any()), cold: schema.maybe(schema.any()), + frozen: schema.maybe(schema.any()), delete: schema.maybe(schema.any()), }), }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9e4ec1dff0e30..0cbaf1a7921c2 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10046,7 +10046,6 @@ "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "コールドティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。安価なハードウェアのコールドフェーズにデータを格納します。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "インデックスを凍結", - "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "レプリカの数", "xpack.indexLifecycleMgmt.common.dataTier.title": "データ割り当て", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "キャンセル", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "削除", @@ -10061,16 +10060,10 @@ "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "コールドフェーズを有効にする", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription": "データをコールドティアに移動します。これは、検索パフォーマンスよりもコスト削減を優先するように最適化されています。通常、コールドフェーズではデータが読み取り専用です。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle": "コールドフェーズ", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "インデックスを読み取り専用にし、メモリー消費量を最小化します。", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "凍結", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "ノード属性に基づいてデータを移動します。", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "カスタム", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "コールドティアのノードにデータを移動します。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input": "コールドノードを使用 (推奨) ", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText": "コールドフェーズにデータを移動しないでください。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input": "オフ", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText": "ノード属性に基づいてデータを移動します。", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input": "カスタム", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText": "ウォームティアのノードにデータを移動します。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input": "ウォームノードを使用 (推奨) ", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText": "ウォームフェーズにデータを移動しないでください。", @@ -10183,7 +10176,6 @@ "xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "ライフサイクルポリシー {lifecycleName} の保存中にエラーが発生しました", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "検索可能なスナップショットがホットフェーズで有効な場合には、強制、マージ、縮小、凍結、コールドフェーズの検索可能なスナップショットは許可されません。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "選択したリポジトリで管理されたインデックスのスナップショットを作成し、検索可能なスナップショットとしてマウントします。{learnMoreLink}", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "検索可能なスナップショットリポジドリ", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "検索可能スナップショット", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "検索可能なスナップショットを作成するには、エンタープライズライセンスが必要です。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "エンタープライズライセンスが必要です", @@ -10349,7 +10341,6 @@ "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "このポリシーはウォームフェーズのデータを{tier}ティアノードに移動します。", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "ウォームティアに割り当てられているノードがありません", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "頻度が低い読み取り専用アクセス用に最適化されたノードにデータを移動します。", - "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "レプリカの数", "xpack.infra.alerting.alertDropdownTitle": "アラート", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "なし (グループなし) ", "xpack.infra.alerting.alertFlyout.groupByLabel": "グループ分けの条件", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e1f700ae2a556..f11aa3fc3da6e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10173,7 +10173,6 @@ "xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle": "没有分配到冷层的节点", "xpack.indexLifecycleMgmt.coldPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。将处于冷阶段的数据存储在成本较低的硬件上。", "xpack.indexLifecycleMgmt.coldPhase.freezeIndexLabel": "冻结索引", - "xpack.indexLifecycleMgmt.coldPhase.numberOfReplicasLabel": "副本分片数目", "xpack.indexLifecycleMgmt.common.dataTier.title": "数据分配", "xpack.indexLifecycleMgmt.confirmDelete.cancelButton": "取消", "xpack.indexLifecycleMgmt.confirmDelete.deleteButton": "删除", @@ -10188,16 +10187,10 @@ "xpack.indexLifecycleMgmt.editPolicy.coldPhase.activateColdPhaseSwitchLabel": "激活冷阶段", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseDescription": "将数据移到经过优化后节省了成本但牺牲了搜索性能的冷层。数据在冷阶段通常为只读。", "xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle": "冷阶段", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeIndexExplanationText": "使索引只读,并最大限度减小其内存占用。", - "xpack.indexLifecycleMgmt.editPolicy.coldPhase.freezeText": "冻结", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.helpText": "根据节点属性移动数据。", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.customOption.input": "定制", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.helpText": "将数据移到冷层中的节点。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.defaultOption.input": "使用冷节点 (建议) ", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.helpText": "不要移动冷阶段的数据。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.cold.noneOption.input": "关闭", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.helpText": "根据节点属性移动数据。", - "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.customOption.input": "定制", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.helpText": "将数据移到温层中的节点。", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.defaultOption.input": "使用温节点 (建议) ", "xpack.indexLifecycleMgmt.editPolicy.common.dataTierAllocation.warm.noneOption.helpText": "不要移动温阶段的数据。", @@ -10310,7 +10303,6 @@ "xpack.indexLifecycleMgmt.editPolicy.saveErrorMessage": "保存生命周期策略 {lifecycleName} 时出错", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotCalloutBody": "在热阶段启用可搜索快照时,不允许强制合并、缩小、冻结可搜索快照以及将其置入冷阶段。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldDescription": "在所选存储库中拍取受管索引的快照,并将其安装为可搜索快照。{learnMoreLink}", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldLabel": "可搜索快照存储库", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "可搜索快照", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "要创建可搜索快照,需要企业许可证。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "需要企业许可证", @@ -10480,7 +10472,6 @@ "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm": "此策略会改为将温阶段的数据移到{tier}层节点。", "xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotice.warm.title": "没有分配到温层的节点", "xpack.indexLifecycleMgmt.warmPhase.dataTier.description": "将数据移到针对不太频繁的只读访问优化的节点。", - "xpack.indexLifecycleMgmt.warmPhase.numberOfReplicasLabel": "副本分片数目", "xpack.infra.alerting.alertDropdownTitle": "告警", "xpack.infra.alerting.alertFlyout.groupBy.placeholder": "无内容 (未分组) ", "xpack.infra.alerting.alertFlyout.groupByLabel": "分组依据", diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js index ec3f5fe6fe349..e61470cc2cc84 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/fixtures.js @@ -75,8 +75,16 @@ export const getPolicyPayload = (name) => ({ }, }, }, + frozen: { + min_age: '20d', + actions: { + searchable_snapshot: { + snapshot_repository: 'backing_repo', + }, + }, + }, delete: { - min_age: '10d', + min_age: '30d', actions: { wait_for_snapshot: { policy: 'policy', From 95271bf7989dfcd398d52308cb45471c8ad7c50e Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Wed, 10 Mar 2021 15:09:30 -0500 Subject: [PATCH 10/26] [Security Solution][Exceptions] Fixes OS adding method for exception enrichment (#94343) --- x-pack/plugins/lists/common/shared_exports.ts | 1 + .../common/shared_imports.ts | 1 + .../exceptions/add_exception_modal/index.tsx | 18 +++-------------- .../components/exceptions/helpers.test.tsx | 20 +++++++++++++++++++ .../common/components/exceptions/helpers.tsx | 13 ++++++++++++ 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 06e72b0070dfe..23da48b35a9d4 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -44,6 +44,7 @@ export { namespaceType, ExceptionListType, Type, + osType, osTypeArray, OsTypeArray, } from './schemas'; diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index a578fb932068d..aaae0d4dc25ef 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -45,6 +45,7 @@ export { Type, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, + osType, osTypeArray, OsTypeArray, buildExceptionFilter, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 3a2170d126a24..b0ffcb8c5b5b8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -33,7 +33,6 @@ import { import * as i18nCommon from '../../../translations'; import * as i18n from './translations'; import * as sharedI18n from '../translations'; -import { osTypeArray, OsTypeArray } from '../../../../../common/shared_imports'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import { useKibana } from '../../../lib/kibana'; import { ExceptionBuilderComponent } from '../builder'; @@ -50,6 +49,7 @@ import { defaultEndpointExceptionItems, entryHasListType, entryHasNonEcsType, + retrieveAlertOsTypes, } from '../helpers'; import { ErrorInfo, ErrorCallout } from '../error_callout'; import { AlertData, ExceptionsBuilderExceptionItem } from '../types'; @@ -291,18 +291,6 @@ export const AddExceptionModal = memo(function AddExceptionModal({ [setShouldBulkCloseAlert] ); - const retrieveAlertOsTypes = useCallback((): OsTypeArray => { - const osDefaults: OsTypeArray = ['windows', 'macos']; - if (alertData != null) { - const osTypes = alertData.host && alertData.host.os && alertData.host.os.family; - if (osTypeArray.is(osTypes) && osTypes != null && osTypes.length > 0) { - return osTypes; - } - return osDefaults; - } - return osDefaults; - }, [alertData]); - const enrichExceptionItems = useCallback((): Array< ExceptionListItemSchema | CreateExceptionListItemSchema > => { @@ -312,11 +300,11 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ? enrichNewExceptionItemsWithComments(exceptionItemsToAdd, [{ comment }]) : exceptionItemsToAdd; if (exceptionListType === 'endpoint') { - const osTypes = retrieveAlertOsTypes(); + const osTypes = retrieveAlertOsTypes(alertData); enriched = lowercaseHashValues(enrichExceptionItemsWithOS(enriched, osTypes)); } return enriched; - }, [comment, exceptionItemsToAdd, exceptionListType, retrieveAlertOsTypes]); + }, [comment, exceptionItemsToAdd, exceptionListType, alertData]); const onAddExceptionConfirm = useCallback((): void => { if (addOrUpdateExceptionItems != null) { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index f21f189438890..3463f521655cb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -29,6 +29,7 @@ import { defaultEndpointExceptionItems, getFileCodeSignature, getProcessCodeSignature, + retrieveAlertOsTypes, } from './helpers'; import { AlertData, EmptyEntry } from './types'; import { @@ -533,6 +534,25 @@ describe('Exception helpers', () => { }); }); + describe('#retrieveAlertOsTypes', () => { + test('it should retrieve os type if alert data is provided', () => { + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + host: { os: { family: 'windows' } }, + }; + const result = retrieveAlertOsTypes(alertDataMock); + const expected = ['windows']; + expect(result).toEqual(expected); + }); + + test('it should return default os types if alert data is not provided', () => { + const result = retrieveAlertOsTypes(); + const expected = ['windows', 'macos']; + expect(result).toEqual(expected); + }); + }); + describe('#entryHasListType', () => { test('it should return false with an empty array', () => { const payload: ExceptionListItemSchema[] = []; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 04502d1e16204..43c3b6c082f1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -13,6 +13,7 @@ import uuid from 'uuid'; import * as i18n from './translations'; import { + AlertData, BuilderEntry, CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem, @@ -39,6 +40,7 @@ import { EntryNested, OsTypeArray, EntriesArray, + osType, } from '../../../shared_imports'; import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; import { validate } from '../../../../common/validate'; @@ -359,6 +361,17 @@ export const enrichExceptionItemsWithOS = ( }); }; +export const retrieveAlertOsTypes = (alertData?: AlertData): OsTypeArray => { + const osDefaults: OsTypeArray = ['windows', 'macos']; + if (alertData != null) { + const os = alertData.host && alertData.host.os && alertData.host.os.family; + if (os != null) { + return osType.is(os) ? [os] : osDefaults; + } + } + return osDefaults; +}; + /** * Returns given exceptionItems with all hash-related entries lowercased */ From ebd92a6e5dfd070d00b815df5b5f8b350aca09a7 Mon Sep 17 00:00:00 2001 From: Michael Olorunnisola Date: Wed, 10 Mar 2021 15:10:27 -0500 Subject: [PATCH 11/26] [Security_Solution][Telemetry] - Update endpoint usage to use agentService (#93829) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security_solution/server/plugin.ts | 13 +- .../server/usage/collector.ts | 3 +- .../server/usage/endpoints/endpoint.mocks.ts | 121 ++++++++---------- .../server/usage/endpoints/endpoint.test.ts | 89 ++++++++----- .../usage/endpoints/fleet_saved_objects.ts | 42 +++--- .../server/usage/endpoints/index.ts | 24 ++-- .../security_solution/server/usage/types.ts | 10 +- .../apps/endpoint/endpoint_telemetry.ts | 4 +- 8 files changed, 164 insertions(+), 142 deletions(-) diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 905078f676eef..8b9afa7cacc0c 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -162,19 +162,20 @@ export class Plugin implements IPlugin => Promise.resolve(config), + }; + initUsageCollectors({ core, + endpointAppContext: endpointContext, kibanaIndex: globalConfig.kibana.index, ml: plugins.ml, usageCollection: plugins.usageCollection, }); - const endpointContext: EndpointAppContext = { - logFactory: this.context.logger, - service: this.endpointAppContextService, - config: (): Promise => Promise.resolve(config), - }; - const router = core.http.createRouter(); core.http.registerRouteHandlerContext( APP_ID, diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index 981101bf733c7..53fa1a1571835 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -31,6 +31,7 @@ export async function getInternalSavedObjectsClient(core: CoreSetup) { export const registerCollector: RegisterCollector = ({ core, + endpointAppContext, kibanaIndex, ml, usageCollection, @@ -138,7 +139,7 @@ export const registerCollector: RegisterCollector = ({ const [detections, detectionMetrics, endpoints] = await Promise.allSettled([ fetchDetectionsUsage(kibanaIndex, esClient, ml, savedObjectsClient), fetchDetectionsMetrics(ml, savedObjectsClient), - getEndpointTelemetryFromFleet(internalSavedObjectsClient), + getEndpointTelemetryFromFleet(savedObjectsClient, endpointAppContext, esClient), ]); return { diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts index 2c6a1bb69cf27..caa4549fbf31d 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.mocks.ts @@ -7,10 +7,7 @@ import { SavedObjectsFindResponse } from 'src/core/server'; import { AgentEventSOAttributes } from './../../../../fleet/common/types/models/agent'; -import { - AGENT_SAVED_OBJECT_TYPE, - AGENT_EVENT_SAVED_OBJECT_TYPE, -} from '../../../../fleet/common/constants/agent'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../../fleet/common/constants/agent'; import { Agent } from '../../../../fleet/common'; import { FLEET_ENDPOINT_PACKAGE_CONSTANT } from './fleet_saved_objects'; @@ -36,84 +33,68 @@ export const MockOSFullName = 'somePlatformFullName'; export const mockFleetObjectsResponse = ( hasDuplicates = true, lastCheckIn = new Date().toISOString() -): SavedObjectsFindResponse => ({ +): { agents: Agent[]; total: number; page: number; perPage: number } | undefined => ({ page: 1, - per_page: 20, + perPage: 20, total: 1, - saved_objects: [ + agents: [ { - type: AGENT_SAVED_OBJECT_TYPE, + active: true, id: testAgentId, - attributes: { - active: true, - id: testAgentId, - policy_id: 'randoAgentPolicyId', - type: 'PERMANENT', - user_provided_metadata: {}, - enrolled_at: lastCheckIn, - current_error_events: [], - local_metadata: { - elastic: { - agent: { - id: testAgentId, - }, - }, - host: { - hostname: testHostName, - name: testHostName, - id: testHostId, - }, - os: { - platform: MockOSPlatform, - version: MockOSVersion, - name: MockOSName, - full: MockOSFullName, + policy_id: 'randoAgentPolicyId', + type: 'PERMANENT', + user_provided_metadata: {}, + enrolled_at: lastCheckIn, + current_error_events: [], + local_metadata: { + elastic: { + agent: { + id: testAgentId, }, }, - packages: [FLEET_ENDPOINT_PACKAGE_CONSTANT, 'system'], - last_checkin: lastCheckIn, + host: { + hostname: testHostName, + name: testHostName, + id: testHostId, + }, + os: { + platform: MockOSPlatform, + version: MockOSVersion, + name: MockOSName, + full: MockOSFullName, + }, }, - references: [], - updated_at: lastCheckIn, - version: 'WzI4MSwxXQ==', - score: 0, + packages: [FLEET_ENDPOINT_PACKAGE_CONSTANT, 'system'], + last_checkin: lastCheckIn, }, { - type: AGENT_SAVED_OBJECT_TYPE, - id: testAgentId, - attributes: { - active: true, - id: 'oldTestAgentId', - policy_id: 'randoAgentPolicyId', - type: 'PERMANENT', - user_provided_metadata: {}, - enrolled_at: lastCheckIn, - current_error_events: [], - local_metadata: { - elastic: { - agent: { - id: 'oldTestAgentId', - }, - }, - host: { - hostname: hasDuplicates ? testHostName : 'oldRandoHostName', - name: hasDuplicates ? testHostName : 'oldRandoHostName', - id: hasDuplicates ? testHostId : 'oldRandoHostId', - }, - os: { - platform: MockOSPlatform, - version: MockOSVersion, - name: MockOSName, - full: MockOSFullName, + active: true, + id: 'oldTestAgentId', + policy_id: 'randoAgentPolicyId', + type: 'PERMANENT', + user_provided_metadata: {}, + enrolled_at: lastCheckIn, + current_error_events: [], + local_metadata: { + elastic: { + agent: { + id: 'oldTestAgentId', }, }, - packages: [FLEET_ENDPOINT_PACKAGE_CONSTANT, 'system'], - last_checkin: lastCheckIn, + host: { + hostname: hasDuplicates ? testHostName : 'oldRandoHostName', + name: hasDuplicates ? testHostName : 'oldRandoHostName', + id: hasDuplicates ? testHostId : 'oldRandoHostId', + }, + os: { + platform: MockOSPlatform, + version: MockOSVersion, + name: MockOSName, + full: MockOSFullName, + }, }, - references: [], - updated_at: lastCheckIn, - version: 'WzI4MSwxXQ==', - score: 0, + packages: [FLEET_ENDPOINT_PACKAGE_CONSTANT, 'system'], + last_checkin: lastCheckIn, }, ], }); diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts index aaf85a0201478..1541cb128f60c 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/endpoint.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; import { mockFleetObjectsResponse, mockFleetEventsObjectsResponse, @@ -13,23 +13,34 @@ import { MockOSPlatform, MockOSVersion, } from './endpoint.mocks'; -import { ISavedObjectsRepository, SavedObjectsFindResponse } from 'src/core/server'; +import { SavedObjectsClientContract, SavedObjectsFindResponse } from 'src/core/server'; import { AgentEventSOAttributes } from '../../../../fleet/common/types/models/agent'; import { Agent } from '../../../../fleet/common'; import * as endpointTelemetry from './index'; import * as fleetSavedObjects from './fleet_saved_objects'; +import { createMockEndpointAppContext } from '../../endpoint/mocks'; +import { EndpointAppContext } from '../../endpoint/types'; describe('test security solution endpoint telemetry', () => { - let mockSavedObjectsRepository: jest.Mocked; - let getFleetSavedObjectsMetadataSpy: jest.SpyInstance>>; + let mockSavedObjectsClient: jest.Mocked; + let mockEndpointAppContext: EndpointAppContext; + let mockEsClient: ReturnType; + let getEndpointIntegratedFleetMetadataSpy: jest.SpyInstance< + Promise<{ agents: Agent[]; total: number; page: number; perPage: number } | undefined> + >; let getLatestFleetEndpointEventSpy: jest.SpyInstance< Promise> >; beforeAll(() => { getLatestFleetEndpointEventSpy = jest.spyOn(fleetSavedObjects, 'getLatestFleetEndpointEvent'); - getFleetSavedObjectsMetadataSpy = jest.spyOn(fleetSavedObjects, 'getFleetSavedObjectsMetadata'); - mockSavedObjectsRepository = savedObjectsRepositoryMock.create(); + getEndpointIntegratedFleetMetadataSpy = jest.spyOn( + fleetSavedObjects, + 'getEndpointIntegratedFleetMetadata' + ); + mockSavedObjectsClient = savedObjectsClientMock.create(); + mockEndpointAppContext = createMockEndpointAppContext(); + mockEsClient = elasticsearchServiceMock.createElasticsearchClient(); }); afterAll(() => { @@ -55,28 +66,32 @@ describe('test security solution endpoint telemetry', () => { describe('when a request for endpoint agents fails', () => { it('should return an empty object', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.reject(Error('No agents for you')) ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); - expect(getFleetSavedObjectsMetadataSpy).toHaveBeenCalled(); + expect(getEndpointIntegratedFleetMetadataSpy).toHaveBeenCalled(); expect(endpointUsage).toEqual({}); }); }); describe('when an agent has not been installed', () => { it('should return the default shape if no agents are found', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => - Promise.resolve({ saved_objects: [], total: 0, per_page: 0, page: 0 }) + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => + Promise.resolve({ agents: [], total: 0, perPage: 0, page: 0 }) ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); - expect(getFleetSavedObjectsMetadataSpy).toHaveBeenCalled(); + expect(getEndpointIntegratedFleetMetadataSpy).toHaveBeenCalled(); expect(endpointUsage).toEqual({ total_installed: 0, active_within_last_24_hours: 0, @@ -95,7 +110,7 @@ describe('test security solution endpoint telemetry', () => { describe('when agent(s) have been installed', () => { describe('when a request for events has failed', () => { it('should show only one endpoint installed but it is inactive', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -103,7 +118,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -129,7 +146,7 @@ describe('test security solution endpoint telemetry', () => { describe('when a request for events is successful', () => { it('should show one endpoint installed but endpoint has failed to run', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -137,7 +154,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -161,7 +180,7 @@ describe('test security solution endpoint telemetry', () => { }); it('should show two endpoints installed but both endpoints have failed to run', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse(false)) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -169,7 +188,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 2, @@ -197,7 +218,7 @@ describe('test security solution endpoint telemetry', () => { twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); const twoDaysAgoISOString = twoDaysAgo.toISOString(); - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse(false, twoDaysAgoISOString)) ); getLatestFleetEndpointEventSpy.mockImplementation( @@ -205,7 +226,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 2, @@ -229,7 +252,7 @@ describe('test security solution endpoint telemetry', () => { }); it('should show one endpoint installed and endpoint is running', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -237,7 +260,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -262,7 +287,7 @@ describe('test security solution endpoint telemetry', () => { describe('malware policy', () => { it('should have failed to enable', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -272,7 +297,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -296,7 +323,7 @@ describe('test security solution endpoint telemetry', () => { }); it('should be enabled successfully', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -304,7 +331,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, @@ -328,7 +357,7 @@ describe('test security solution endpoint telemetry', () => { }); it('should be disabled successfully', async () => { - getFleetSavedObjectsMetadataSpy.mockImplementation(() => + getEndpointIntegratedFleetMetadataSpy.mockImplementation(() => Promise.resolve(mockFleetObjectsResponse()) ); getLatestFleetEndpointEventSpy.mockImplementation(() => @@ -338,7 +367,9 @@ describe('test security solution endpoint telemetry', () => { ); const endpointUsage = await endpointTelemetry.getEndpointTelemetryFromFleet( - mockSavedObjectsRepository + mockSavedObjectsClient, + mockEndpointAppContext, + mockEsClient ); expect(endpointUsage).toEqual({ total_installed: 1, diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts index e96ce0b2dda76..7e3620ec0ae04 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/fleet_saved_objects.ts @@ -5,38 +5,36 @@ * 2.0. */ -import { ISavedObjectsRepository } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import { AgentService } from '../../../../fleet/server'; import { AgentEventSOAttributes } from './../../../../fleet/common/types/models/agent'; -import { - AGENT_SAVED_OBJECT_TYPE, - AGENT_EVENT_SAVED_OBJECT_TYPE, -} from './../../../../fleet/common/constants/agent'; -import { Agent, defaultPackages as FleetDefaultPackages } from '../../../../fleet/common'; +import { AGENT_EVENT_SAVED_OBJECT_TYPE } from './../../../../fleet/common/constants/agent'; +import { defaultPackages as FleetDefaultPackages } from '../../../../fleet/common'; export const FLEET_ENDPOINT_PACKAGE_CONSTANT = FleetDefaultPackages.Endpoint; -export const getFleetSavedObjectsMetadata = async (savedObjectsClient: ISavedObjectsRepository) => - savedObjectsClient.find({ - // Get up to 10000 agents with endpoint installed - type: AGENT_SAVED_OBJECT_TYPE, - fields: [ - 'packages', - 'last_checkin', - 'local_metadata.agent.id', - 'local_metadata.host.id', - 'local_metadata.host.name', - 'local_metadata.host.hostname', - 'local_metadata.elastic.agent.id', - 'local_metadata.os', - ], - filter: `${AGENT_SAVED_OBJECT_TYPE}.attributes.packages: ${FLEET_ENDPOINT_PACKAGE_CONSTANT}`, +export const getEndpointIntegratedFleetMetadata = async ( + agentService: AgentService | undefined, + esClient: ElasticsearchClient +) => { + return agentService?.listAgents(esClient, { + kuery: `(packages : ${FLEET_ENDPOINT_PACKAGE_CONSTANT})`, perPage: 10000, + showInactive: false, sortField: 'enrolled_at', sortOrder: 'desc', }); +}; + +/* + TODO: AS OF 7.13, this access will no longer work due to the enabling of fleet server. An alternative route will have + to be discussed to retrieve the policy data we need, as well as when the endpoint was last active, which is obtained + via the last endpoint 'check in' event that was sent to fleet. Also, the only policy currently tracked is `malware`, + but the hope is to add more, so a better/more scalable solution would be desirable. +*/ export const getLatestFleetEndpointEvent = async ( - savedObjectsClient: ISavedObjectsRepository, + savedObjectsClient: SavedObjectsClientContract, agentId: string ) => savedObjectsClient.find({ diff --git a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts index 48cb50a493edf..94ff168ffffc8 100644 --- a/x-pack/plugins/security_solution/server/usage/endpoints/index.ts +++ b/x-pack/plugins/security_solution/server/usage/endpoints/index.ts @@ -6,11 +6,15 @@ */ import { cloneDeep } from 'lodash'; -import { ISavedObjectsRepository } from 'src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; import { SavedObject } from './../../../../../../src/core/types/saved_objects'; import { Agent, NewAgentEvent } from './../../../../fleet/common/types/models/agent'; import { AgentMetadata } from '../../../../fleet/common/types/models/agent'; -import { getFleetSavedObjectsMetadata, getLatestFleetEndpointEvent } from './fleet_saved_objects'; +import { + getEndpointIntegratedFleetMetadata, + getLatestFleetEndpointEvent, +} from './fleet_saved_objects'; +import { EndpointAppContext } from '../../endpoint/types'; export interface AgentOSMetadataTelemetry { full_name: string; @@ -108,7 +112,7 @@ export const updateEndpointOSTelemetry = ( * the same time span. */ export const updateEndpointDailyActiveCount = ( - latestEndpointEvent: SavedObject, + latestEndpointEvent: SavedObject, // TODO: This information will be lost in 7.13, need to find an alternative route. lastAgentCheckin: Agent['last_checkin'], currentCount: number ) => { @@ -193,19 +197,22 @@ export const updateEndpointPolicyTelemetry = ( }; /** - * @description This aggregates the telemetry details from the two fleet savedObject sources, `fleet-agents` and `fleet-agent-events` to populate + * @description This aggregates the telemetry details from the fleet agent service `listAgents` and the fleet saved object `fleet-agent-events` to populate * the telemetry details for endpoint. Since we cannot access our own indices due to `kibana_system` not having access, this is the best alternative. * Once the data is requested, we iterate over all agents with endpoints registered, and then request the events for each active agent (within last 24 hours) * to confirm whether or not the endpoint is still active */ export const getEndpointTelemetryFromFleet = async ( - soClient: ISavedObjectsRepository + soClient: SavedObjectsClientContract, + endpointAppContext: EndpointAppContext, + esClient: ElasticsearchClient ): Promise => { // Retrieve every agent (max 10000) that references the endpoint as an installed package. It will not be listed if it was never installed let endpointAgents; + const agentService = endpointAppContext.service.getAgentService(); try { - const response = await getFleetSavedObjectsMetadata(soClient); - endpointAgents = response.saved_objects; + const response = await getEndpointIntegratedFleetMetadata(agentService, esClient); + endpointAgents = response?.agents ?? []; } catch (error) { // Better to provide an empty object rather than default telemetry as this better informs us of an error return {}; @@ -225,8 +232,7 @@ export const getEndpointTelemetryFromFleet = async ( for (let i = 0; i < endpointAgentsCount; i += 1) { try { - const { attributes: metadataAttributes } = endpointAgents[i]; - const { last_checkin: lastCheckin, local_metadata: localMetadata } = metadataAttributes; + const { last_checkin: lastCheckin, local_metadata: localMetadata } = endpointAgents[i]; const { host, os, elastic } = localMetadata as AgentLocalMetadata; // Although not perfect, the goal is to dedupe hosts to get the most recent data for a host diff --git a/x-pack/plugins/security_solution/server/usage/types.ts b/x-pack/plugins/security_solution/server/usage/types.ts index 562b6a5278f64..c06c8a4722cd7 100644 --- a/x-pack/plugins/security_solution/server/usage/types.ts +++ b/x-pack/plugins/security_solution/server/usage/types.ts @@ -6,9 +6,11 @@ */ import { CoreSetup } from 'src/core/server'; +import { EndpointAppContext } from '../endpoint/types'; import { SetupPlugins } from '../plugin'; -export type CollectorDependencies = { kibanaIndex: string; core: CoreSetup } & Pick< - SetupPlugins, - 'ml' | 'usageCollection' ->; +export type CollectorDependencies = { + kibanaIndex: string; + core: CoreSetup; + endpointAppContext: EndpointAppContext; +} & Pick; diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts index 6d0a3255685f2..57f03a197b389 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts @@ -12,7 +12,9 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const telemetryTestResources = getService('telemetryTestResources'); - describe('security solution endpoint telemetry', () => { + // The source of the data for these tests have changed and need to be updated + // There are currently tests in the security_solution application being maintained + describe.skip('security solution endpoint telemetry', () => { after(async () => { await esArchiver.load('empty_kibana'); }); From 6264c563d17bc72127adb200e06b3634a399463d Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 10 Mar 2021 13:23:42 -0700 Subject: [PATCH 12/26] [Maps] convert elasticsearch_utils to TS (#93984) * [Maps] convert elasticsearch_utils to TS * tslint * clean up * i18n cleanup * update elasticsearch_geo_utils tests * fix unit test * review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/common/constants.ts | 18 +- .../elasticsearch_geo_utils.d.ts | 53 ----- .../elasticsearch_geo_utils.test.js | 50 ++--- ...eo_utils.js => elasticsearch_geo_utils.ts} | 195 +++++++++++++----- x-pack/plugins/maps/common/i18n_getters.ts | 3 +- .../es_geo_grid_source.test.ts | 10 +- .../es_search_source/es_search_source.tsx | 9 +- .../classes/sources/es_source/es_source.ts | 1 - .../maps/public/classes/sources/source.ts | 7 +- x-pack/plugins/maps/server/mvt/util.ts | 14 +- 10 files changed, 194 insertions(+), 166 deletions(-) delete mode 100644 x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts rename x-pack/plugins/maps/common/elasticsearch_util/{elasticsearch_geo_utils.js => elasticsearch_geo_utils.ts} (70%) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index cfceb3e8b422e..f1e0ac25aa127 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -131,15 +131,15 @@ export enum ES_SPATIAL_RELATIONS { WITHIN = 'WITHIN', } -export const GEO_JSON_TYPE = { - POINT: 'Point', - MULTI_POINT: 'MultiPoint', - LINE_STRING: 'LineString', - MULTI_LINE_STRING: 'MultiLineString', - POLYGON: 'Polygon', - MULTI_POLYGON: 'MultiPolygon', - GEOMETRY_COLLECTION: 'GeometryCollection', -}; +export enum GEO_JSON_TYPE { + POINT = 'Point', + MULTI_POINT = 'MultiPoint', + LINE_STRING = 'LineString', + MULTI_LINE_STRING = 'MultiLineString', + POLYGON = 'Polygon', + MULTI_POLYGON = 'MultiPolygon', + GEOMETRY_COLLECTION = 'GeometryCollection', +} export const POLYGON_COORDINATES_EXTERIOR_INDEX = 0; export const LON_INDEX = 0; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts deleted file mode 100644 index 2a3741146d454..0000000000000 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts +++ /dev/null @@ -1,53 +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 { FeatureCollection, GeoJsonProperties, Polygon } from 'geojson'; -import { MapExtent } from '../descriptor_types'; -import { ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; - -export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent; - -export function turfBboxToBounds(turfBbox: unknown): MapExtent; - -export function clampToLatBounds(lat: number): number; - -export function clampToLonBounds(lon: number): number; - -export function hitsToGeoJson( - hits: Array>, - flattenHit: (elasticSearchHit: Record) => GeoJsonProperties, - geoFieldName: string, - geoFieldType: ES_GEO_FIELD_TYPE, - epochMillisFields: string[] -): FeatureCollection; - -export interface ESBBox { - top_left: number[]; - bottom_right: number[]; -} - -export interface ESGeoBoundingBoxFilter { - geo_bounding_box: { - [geoFieldName: string]: ESBBox; - }; -} - -export interface ESPolygonFilter { - geo_shape: { - [geoFieldName: string]: { - shape: Polygon; - relation: ES_SPATIAL_RELATIONS.INTERSECTS; - }; - }; -} - -export function createExtentFilter( - mapExtent: MapExtent, - geoFieldName: string -): ESPolygonFilter | ESGeoBoundingBoxFilter; - -export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBox; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index 9983bb9b84588..22b8a86158a74 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -397,12 +397,10 @@ describe('createExtentFilter', () => { minLon: -89, }; const filter = createExtentFilter(mapExtent, geoFieldName); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [-89, 39], - bottom_right: [-83, 35], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-89, 39], + bottom_right: [-83, 35], }, }); }); @@ -415,12 +413,10 @@ describe('createExtentFilter', () => { minLon: -190, }; const filter = createExtentFilter(mapExtent, geoFieldName); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [-180, 89], - bottom_right: [180, -89], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-180, 89], + bottom_right: [180, -89], }, }); }); @@ -436,12 +432,10 @@ describe('createExtentFilter', () => { const leftLon = filter.geo_bounding_box.location.top_left[0]; const rightLon = filter.geo_bounding_box.location.bottom_right[0]; expect(leftLon).toBeGreaterThan(rightLon); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [100, 39], - bottom_right: [-160, 35], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [100, 39], + bottom_right: [-160, 35], }, }); }); @@ -457,12 +451,10 @@ describe('createExtentFilter', () => { const leftLon = filter.geo_bounding_box.location.top_left[0]; const rightLon = filter.geo_bounding_box.location.bottom_right[0]; expect(leftLon).toBeGreaterThan(rightLon); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [160, 39], - bottom_right: [-100, 35], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [160, 39], + bottom_right: [-100, 35], }, }); }); @@ -475,12 +467,10 @@ describe('createExtentFilter', () => { minLon: -191, }; const filter = createExtentFilter(mapExtent, geoFieldName); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - top_left: [-180, 39], - bottom_right: [180, 35], - }, + expect(filter.geo_bounding_box).toEqual({ + location: { + top_left: [-180, 39], + bottom_right: [180, 35], }, }); }); diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts similarity index 70% rename from x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js rename to x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts index 47de8850c0e96..f529f187c690c 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts @@ -7,7 +7,12 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +// @ts-expect-error import { parse } from 'wellknown'; +// @ts-expect-error +import turfCircle from '@turf/circle'; +import { Feature, FeatureCollection, Geometry, Polygon, Point, Position } from 'geojson'; +import { BBox } from '@turf/helpers'; import { DECIMAL_DEGREES_PRECISION, ES_GEO_FIELD_TYPE, @@ -18,14 +23,54 @@ import { LAT_INDEX, } from '../constants'; import { getEsSpatialRelationLabel } from '../i18n_getters'; -import { FILTERS } from '../../../../../src/plugins/data/common'; -import turfCircle from '@turf/circle'; +import { Filter, FILTERS } from '../../../../../src/plugins/data/common'; +import { MapExtent } from '../descriptor_types'; const SPATIAL_FILTER_TYPE = FILTERS.SPATIAL_FILTER; -function ensureGeoField(type) { +type Coordinates = Position | Position[] | Position[][] | Position[][][]; + +// Elasticsearch stores more then just GeoJSON. +// 1) geometry.type as lower case string +// 2) circle and envelope types +interface ESGeometry { + type: string; + coordinates: Coordinates; +} + +export interface ESBBox { + top_left: number[]; + bottom_right: number[]; +} + +interface GeoShapeQueryBody { + shape?: Polygon; + relation?: ES_SPATIAL_RELATIONS; + indexed_shape?: PreIndexedShape; +} + +export type GeoFilter = Filter & { + geo_bounding_box?: { + [geoFieldName: string]: ESBBox; + }; + geo_distance?: { + distance: string; + [geoFieldName: string]: Position | { lat: number; lon: number } | string; + }; + geo_shape?: { + [geoFieldName: string]: GeoShapeQueryBody; + }; +}; + +export interface PreIndexedShape { + index: string; + id: string | number; + path: string; +} + +function ensureGeoField(type: string) { const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; - if (!expectedTypes.includes(type)) { + if (!expectedTypes.includes(type as ES_GEO_FIELD_TYPE)) { const errorMessage = i18n.translate( 'xpack.maps.es_geo_utils.unsupportedFieldTypeErrorMessage', { @@ -41,8 +86,8 @@ function ensureGeoField(type) { } } -function ensureGeometryType(type, expectedTypes) { - if (!expectedTypes.includes(type)) { +function ensureGeometryType(type: string, expectedTypes: GEO_JSON_TYPE[]) { + if (!expectedTypes.includes(type as GEO_JSON_TYPE)) { const errorMessage = i18n.translate( 'xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage', { @@ -68,36 +113,48 @@ function ensureGeometryType(type, expectedTypes) { * @param {string} geoFieldType Geometry field type ["geo_point", "geo_shape"] * @returns {number} */ -export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType, epochMillisFields) { - const features = []; - const tmpGeometriesAccumulator = []; +export function hitsToGeoJson( + hits: Array>, + flattenHit: (elasticSearchHit: Record) => Record, + geoFieldName: string, + geoFieldType: ES_GEO_FIELD_TYPE, + epochMillisFields: string[] +): FeatureCollection { + const features: Feature[] = []; + const tmpGeometriesAccumulator: Geometry[] = []; for (let i = 0; i < hits.length; i++) { const properties = flattenHit(hits[i]); - tmpGeometriesAccumulator.length = 0; //truncate accumulator + tmpGeometriesAccumulator.length = 0; // truncate accumulator ensureGeoField(geoFieldType); if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - geoPointToGeometry(properties[geoFieldName], tmpGeometriesAccumulator); + geoPointToGeometry( + properties[geoFieldName] as string | string[] | undefined, + tmpGeometriesAccumulator + ); } else { - geoShapeToGeometry(properties[geoFieldName], tmpGeometriesAccumulator); + geoShapeToGeometry( + properties[geoFieldName] as string | string[] | ESGeometry | ESGeometry[] | undefined, + tmpGeometriesAccumulator + ); } // There is a bug in Elasticsearch API where epoch_millis are returned as a string instead of a number // https://github.com/elastic/elasticsearch/issues/50622 // Convert these field values to integers. - for (let i = 0; i < epochMillisFields.length; i++) { - const fieldName = epochMillisFields[i]; + for (let k = 0; k < epochMillisFields.length; k++) { + const fieldName = epochMillisFields[k]; if (typeof properties[fieldName] === 'string') { - properties[fieldName] = parseInt(properties[fieldName]); + properties[fieldName] = parseInt(properties[fieldName] as string, 10); } } // don't include geometry field value in properties delete properties[geoFieldName]; - //create new geojson Feature for every individual geojson geometry. + // create new geojson Feature for every individual geojson geometry. for (let j = 0; j < tmpGeometriesAccumulator.length; j++) { features.push({ type: 'Feature', @@ -112,7 +169,7 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType, epoc return { type: 'FeatureCollection', - features: features, + features, }; } @@ -120,7 +177,10 @@ export function hitsToGeoJson(hits, flattenHit, geoFieldName, geoFieldType, epoc // Either // 1) Array of latLon strings // 2) latLon string -export function geoPointToGeometry(value, accumulator) { +export function geoPointToGeometry( + value: string[] | string | undefined, + accumulator: Geometry[] +): void { if (!value) { return; } @@ -138,10 +198,10 @@ export function geoPointToGeometry(value, accumulator) { accumulator.push({ type: GEO_JSON_TYPE.POINT, coordinates: [lon, lat], - }); + } as Point); } -export function convertESShapeToGeojsonGeometry(value) { +export function convertESShapeToGeojsonGeometry(value: ESGeometry): Geometry { const geoJson = { type: value.type, coordinates: value.coordinates, @@ -183,12 +243,13 @@ export function convertESShapeToGeojsonGeometry(value) { ); throw new Error(invalidGeometrycollectionError); case 'envelope': + const envelopeCoords = geoJson.coordinates as Position[]; // format defined here https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#_envelope const polygon = formatEnvelopeAsPolygon({ - minLon: geoJson.coordinates[0][0], - maxLon: geoJson.coordinates[1][0], - minLat: geoJson.coordinates[1][1], - maxLat: geoJson.coordinates[0][1], + minLon: envelopeCoords[0][0], + maxLon: envelopeCoords[1][0], + minLat: envelopeCoords[1][1], + maxLat: envelopeCoords[0][1], }); geoJson.type = polygon.type; geoJson.coordinates = polygon.coordinates; @@ -205,10 +266,10 @@ export function convertESShapeToGeojsonGeometry(value) { ); throw new Error(errorMessage); } - return geoJson; + return (geoJson as unknown) as Geometry; } -function convertWKTStringToGeojson(value) { +function convertWKTStringToGeojson(value: string): Geometry { try { return parse(value); } catch (e) { @@ -222,7 +283,10 @@ function convertWKTStringToGeojson(value) { } } -export function geoShapeToGeometry(value, accumulator) { +export function geoShapeToGeometry( + value: string | ESGeometry | string[] | ESGeometry[] | undefined, + accumulator: Geometry[] +): void { if (!value) { return; } @@ -243,8 +307,9 @@ export function geoShapeToGeometry(value, accumulator) { value.type === GEO_JSON_TYPE.GEOMETRY_COLLECTION || value.type === 'geometrycollection' ) { - for (let i = 0; i < value.geometries.length; i++) { - geoShapeToGeometry(value.geometries[i], accumulator); + const geometryCollection = (value as unknown) as { geometries: ESGeometry[] }; + for (let i = 0; i < geometryCollection.geometries.length; i++) { + geoShapeToGeometry(geometryCollection.geometries[i], accumulator); } } else { const geoJson = convertESShapeToGeojsonGeometry(value); @@ -252,7 +317,7 @@ export function geoShapeToGeometry(value, accumulator) { } } -export function makeESBbox({ maxLat, maxLon, minLat, minLon }) { +export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBox { const bottom = clampToLatBounds(minLat); const top = clampToLatBounds(maxLat); let esBbox; @@ -280,11 +345,16 @@ export function makeESBbox({ maxLat, maxLon, minLat, minLon }) { return esBbox; } -export function createExtentFilter(mapExtent, geoFieldName) { - const boundingBox = makeESBbox(mapExtent); +export function createExtentFilter(mapExtent: MapExtent, geoFieldName: string): GeoFilter { return { geo_bounding_box: { - [geoFieldName]: boundingBox, + [geoFieldName]: makeESBbox(mapExtent), + }, + meta: { + alias: null, + disabled: false, + negate: false, + key: geoFieldName, }, }; } @@ -297,6 +367,14 @@ export function createSpatialFilterWithGeometry({ geoFieldName, geoFieldType, relation = ES_SPATIAL_RELATIONS.INTERSECTS, +}: { + preIndexedShape?: PreIndexedShape; + geometry: Polygon; + geometryLabel: string; + indexPatternId: string; + geoFieldName: string; + geoFieldType: ES_GEO_FIELD_TYPE; + relation: ES_SPATIAL_RELATIONS; }) { ensureGeoField(geoFieldType); @@ -315,7 +393,7 @@ export function createSpatialFilterWithGeometry({ alias: `${geoFieldName} ${relationLabel} ${geometryLabel}`, }; - const shapeQuery = { + const shapeQuery: GeoShapeQueryBody = { // geo_shape query with geo_point field only supports intersects relation relation: isGeoPoint ? ES_SPATIAL_RELATIONS.INTERSECTS : relation, }; @@ -341,6 +419,12 @@ export function createDistanceFilterWithMeta({ geoFieldName, indexPatternId, point, +}: { + alias: string; + distanceKm: number; + geoFieldName: string; + indexPatternId: string; + point: Position; }) { const meta = { type: SPATIAL_FILTER_TYPE, @@ -368,7 +452,7 @@ export function createDistanceFilterWithMeta({ }; } -export function roundCoordinates(coordinates) { +export function roundCoordinates(coordinates: Coordinates): void { for (let i = 0; i < coordinates.length; i++) { const value = coordinates[i]; if (Array.isArray(value)) { @@ -382,10 +466,10 @@ export function roundCoordinates(coordinates) { /* * returns Polygon geometry where coordinates define a bounding box that contains the input geometry */ -export function getBoundingBoxGeometry(geometry) { +export function getBoundingBoxGeometry(geometry: Geometry): Polygon { ensureGeometryType(geometry.type, [GEO_JSON_TYPE.POLYGON]); - const exterior = geometry.coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX]; + const exterior = (geometry as Polygon).coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX]; const extent = { minLon: exterior[0][LON_INDEX], minLat: exterior[0][LAT_INDEX], @@ -402,7 +486,7 @@ export function getBoundingBoxGeometry(geometry) { return formatEnvelopeAsPolygon(extent); } -export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { +export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }: MapExtent): Polygon { // GeoJSON mandates that the outer polygon must be counterclockwise to avoid ambiguous polygons // when the shape crosses the dateline const lonDelta = maxLon - minLon; @@ -410,25 +494,25 @@ export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { const right = lonDelta > 360 ? 180 : maxLon; const top = clampToLatBounds(maxLat); const bottom = clampToLatBounds(minLat); - const topLeft = [left, top]; - const bottomLeft = [left, bottom]; - const bottomRight = [right, bottom]; - const topRight = [right, top]; + const topLeft = [left, top] as Position; + const bottomLeft = [left, bottom] as Position; + const bottomRight = [right, bottom] as Position; + const topRight = [right, top] as Position; return { type: GEO_JSON_TYPE.POLYGON, coordinates: [[topLeft, bottomLeft, bottomRight, topRight, topLeft]], - }; + } as Polygon; } -export function clampToLatBounds(lat) { +export function clampToLatBounds(lat: number): number { return clamp(lat, -89, 89); } -export function clampToLonBounds(lon) { +export function clampToLonBounds(lon: number): number { return clamp(lon, -180, 180); } -export function clamp(val, min, max) { +export function clamp(val: number, min: number, max: number): number { if (val > max) { return max; } else if (val < min) { @@ -438,25 +522,26 @@ export function clamp(val, min, max) { } } -export function extractFeaturesFromFilters(filters) { - const features = []; +export function extractFeaturesFromFilters(filters: GeoFilter[]): Feature[] { + const features: Feature[] = []; filters .filter((filter) => { return filter.meta.key && filter.meta.type === SPATIAL_FILTER_TYPE; }) .forEach((filter) => { + const geoFieldName = filter.meta.key!; let geometry; - if (filter.geo_distance && filter.geo_distance[filter.meta.key]) { + if (filter.geo_distance && filter.geo_distance[geoFieldName]) { const distanceSplit = filter.geo_distance.distance.split('km'); const distance = parseFloat(distanceSplit[0]); - const circleFeature = turfCircle(filter.geo_distance[filter.meta.key], distance); + const circleFeature = turfCircle(filter.geo_distance[geoFieldName], distance); geometry = circleFeature.geometry; } else if ( filter.geo_shape && - filter.geo_shape[filter.meta.key] && - filter.geo_shape[filter.meta.key].shape + filter.geo_shape[geoFieldName] && + filter.geo_shape[geoFieldName].shape ) { - geometry = filter.geo_shape[filter.meta.key].shape; + geometry = filter.geo_shape[geoFieldName].shape; } else { // do not know how to convert spatial filter to geometry // this includes pre-indexed shapes @@ -475,7 +560,7 @@ export function extractFeaturesFromFilters(filters) { return features; } -export function scaleBounds(bounds, scaleFactor) { +export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent { const width = bounds.maxLon - bounds.minLon; const height = bounds.maxLat - bounds.minLat; return { @@ -486,7 +571,7 @@ export function scaleBounds(bounds, scaleFactor) { }; } -export function turfBboxToBounds(turfBbox) { +export function turfBboxToBounds(turfBbox: BBox): MapExtent { return { minLon: turfBbox[0], minLat: turfBbox[1], diff --git a/x-pack/plugins/maps/common/i18n_getters.ts b/x-pack/plugins/maps/common/i18n_getters.ts index 0e1923bb26545..4e9537a12647f 100644 --- a/x-pack/plugins/maps/common/i18n_getters.ts +++ b/x-pack/plugins/maps/common/i18n_getters.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; -import { $Values } from '@kbn/utility-types'; import { ES_SPATIAL_RELATIONS } from './constants'; export function getAppTitle() { @@ -34,7 +33,7 @@ export function getUrlLabel() { }); } -export function getEsSpatialRelationLabel(spatialRelation: $Values) { +export function getEsSpatialRelationLabel(spatialRelation: ES_SPATIAL_RELATIONS) { switch (spatialRelation) { case ES_SPATIAL_RELATIONS.INTERSECTS: return i18n.translate('xpack.maps.common.esSpatialRelation.intersectsLabel', { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 6696140a5d852..5ac487c713173 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -206,6 +206,12 @@ describe('ESGeoGridSource', () => { expect(getProperty('filter')).toEqual([ { geo_bounding_box: { bar: { bottom_right: [180, -82.67628], top_left: [-180, 82.67628] } }, + meta: { + alias: null, + disabled: false, + key: 'bar', + negate: false, + }, }, ]); expect(getProperty('aggs')).toEqual({ @@ -277,7 +283,7 @@ describe('ESGeoGridSource', () => { expect(urlTemplateWithMeta.minSourceZoom).toBe(0); expect(urlTemplateWithMeta.maxSourceZoom).toBe(24); expect(urlTemplateWithMeta.urlTemplate).toBe( - "rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628)))))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point" + "rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point" ); }); @@ -288,7 +294,7 @@ describe('ESGeoGridSource', () => { }); expect(urlTemplateWithMeta.urlTemplate).toBe( - "rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628)))))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1" + "rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628))),meta:(alias:!n,disabled:!f,key:bar,negate:!f)))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point&searchSessionId=1" ); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 785b00c06dd54..b3ecdbf51f3c3 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -14,7 +14,12 @@ import { IFieldType, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties } from 'geojson'; import { AbstractESSource } from '../es_source'; import { getHttp, getSearchService } from '../../../kibana_services'; -import { addFieldToDSL, getField, hitsToGeoJson } from '../../../../common/elasticsearch_util'; +import { + addFieldToDSL, + getField, + hitsToGeoJson, + PreIndexedShape, +} from '../../../../common/elasticsearch_util'; // @ts-expect-error import { UpdateSourceEditor } from './update_source_editor'; @@ -43,7 +48,7 @@ import { VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; -import { ImmutableSourceProperty, PreIndexedShape, SourceEditorArgs } from '../source'; +import { ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { IField } from '../../fields/field'; import { GeoJsonWithMeta, diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 6b99f1f8860c0..0936cdc50b4c0 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -238,7 +238,6 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource : searchFilters.buffer; const extentFilter = createExtentFilter(buffer, geoField.name); - // @ts-expect-error allFilters.push(extentFilter); } if (searchFilters.applyGlobalTime && (await this.isTimeAware())) { diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index d9eda86428701..7c2aaf714c34e 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -18,6 +18,7 @@ import { FieldFormatter, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; import { AbstractSourceDescriptor } from '../../../common/descriptor_types'; import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view'; import { LICENSED_FEATURES } from '../../licensed_features'; +import { PreIndexedShape } from '../../../common/elasticsearch_util'; export type SourceEditorArgs = { onChange: (...args: OnSourceChangeArgs[]) => void; @@ -35,12 +36,6 @@ export type Attribution = { label: string; }; -export type PreIndexedShape = { - index: string; - id: string | number; - path: string; -}; - export interface ISource { destroy(): void; getDisplayName(): Promise; diff --git a/x-pack/plugins/maps/server/mvt/util.ts b/x-pack/plugins/maps/server/mvt/util.ts index 5d75cebb0facd..b3dc606ba3f11 100644 --- a/x-pack/plugins/maps/server/mvt/util.ts +++ b/x-pack/plugins/maps/server/mvt/util.ts @@ -12,10 +12,12 @@ // - only fields from the response are packed in the tile (more efficient) // - query-dsl submitted from the client, which was generated by the IndexPattern // todo: Ideally, this should adapt/reuse from https://github.com/elastic/kibana/blob/52b42a81faa9dd5c102b9fbb9a645748c3623121/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts#L26 -import { GeoJsonProperties } from 'geojson'; -export function flattenHit(geometryField: string, hit: Record): GeoJsonProperties { - const flat: GeoJsonProperties = {}; +export function flattenHit( + geometryField: string, + hit: Record +): Record { + const flat: Record = {}; if (hit) { flattenSource(flat, '', hit._source as Record, geometryField); if (hit.fields) { @@ -30,11 +32,11 @@ export function flattenHit(geometryField: string, hit: Record): } function flattenSource( - accum: GeoJsonProperties, + accum: Record, path: string, properties: Record = {}, geometryField: string -): GeoJsonProperties { +): Record { accum = accum || {}; for (const key in properties) { if (properties.hasOwnProperty(key)) { @@ -58,7 +60,7 @@ function flattenSource( return accum; } -function flattenFields(accum: GeoJsonProperties = {}, fields: Array>) { +function flattenFields(accum: Record = {}, fields: Array>) { accum = accum || {}; for (const key in fields) { if (fields.hasOwnProperty(key)) { From 68adc48c7ef0f65fbbe5a469b94965de70a7474a Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 10 Mar 2021 13:57:50 -0700 Subject: [PATCH 13/26] skip test failing es promotion (#94367) --- .../security_and_spaces/tests/create_index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts index a319c30fa20de..919be0fcf311c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts @@ -21,7 +21,8 @@ export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('create_index', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/94367 + describe.skip('create_index', () => { afterEach(async () => { await deleteSignalsIndex(supertest); }); From fb199e35845997c2c45820932343d90683627eed Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 10 Mar 2021 13:01:12 -0800 Subject: [PATCH 14/26] [ML] Add latest transform to intro text (#94039) --- .../transform_management/transform_management_section.tsx | 2 +- x-pack/plugins/transform/public/register_feature.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index bcb07c8069ab2..fa1a42b94f0b7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -105,7 +105,7 @@ export const TransformManagement: FC = () => { diff --git a/x-pack/plugins/transform/public/register_feature.ts b/x-pack/plugins/transform/public/register_feature.ts index 2702ef9f616d6..d105424052411 100644 --- a/x-pack/plugins/transform/public/register_feature.ts +++ b/x-pack/plugins/transform/public/register_feature.ts @@ -20,7 +20,7 @@ export const registerFeature = (home: HomePublicPluginSetup) => { }), description: i18n.translate('xpack.transform.transformsDescription', { defaultMessage: - 'Use transforms to pivot existing Elasticsearch indices into summarized or entity-centric indices.', + 'Use transforms to pivot existing Elasticsearch indices into summarized entity-centric indices or to create an indexed view of the latest documents for fast access.', }), icon: 'managementApp', // there is currently no Transforms icon, so using the general management app icon path: '/app/management/data/transform', From 5acf15dccd3abb5dc664ede3868027c7895e2c89 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Wed, 10 Mar 2021 17:46:14 -0400 Subject: [PATCH 15/26] [Workplace Search] Deduplicate icons (#94359) * Remove redundant "_" from icon names * Move all icons from sources_full_bleed to source_icons Overwrite existing icons in case of conflicts * Remove fullbleed prop from source_icon * Minimize the only unminimized icon * Remove unused icons --- .../shared/assets/source_icons/confluence.svg | 2 +- .../shared/assets/source_icons/crawler.svg | 1 - .../shared/assets/source_icons/custom.svg | 2 +- .../shared/assets/source_icons/drive.svg | 1 - .../shared/assets/source_icons/dropbox.svg | 2 +- .../shared/assets/source_icons/github.svg | 2 +- .../shared/assets/source_icons/gmail.svg | 2 +- .../shared/assets/source_icons/google.svg | 1 - .../assets/source_icons/google_drive.svg | 2 +- .../shared/assets/source_icons/index.ts | 18 ++------ .../shared/assets/source_icons/jira.svg | 2 +- .../assets/source_icons/jira_server.svg | 2 +- .../shared/assets/source_icons/office365.svg | 1 - .../shared/assets/source_icons/one_drive.svg | 1 - .../onedrive.svg | 0 .../shared/assets/source_icons/outlook.svg | 1 - .../shared/assets/source_icons/people.svg | 1 - .../shared/assets/source_icons/salesforce.svg | 2 +- .../assets/source_icons/service_now.svg | 1 - .../servicenow.svg | 0 .../assets/source_icons/share_circle.svg | 4 +- .../assets/source_icons/share_point.svg | 1 - .../sharepoint.svg | 0 .../shared/assets/source_icons/slack.svg | 2 +- .../shared/assets/source_icons/zendesk.svg | 2 +- .../shared/assets/sources_full_bleed/box.svg | 1 - .../assets/sources_full_bleed/confluence.svg | 1 - .../assets/sources_full_bleed/custom.svg | 1 - .../assets/sources_full_bleed/dropbox.svg | 1 - .../assets/sources_full_bleed/github.svg | 1 - .../assets/sources_full_bleed/gmail.svg | 1 - .../sources_full_bleed/google_drive.svg | 1 - .../shared/assets/sources_full_bleed/index.ts | 45 ------------------- .../shared/assets/sources_full_bleed/jira.svg | 1 - .../assets/sources_full_bleed/jira_server.svg | 1 - .../assets/sources_full_bleed/salesforce.svg | 1 - .../assets/sources_full_bleed/slack.svg | 1 - .../assets/sources_full_bleed/zendesk.svg | 1 - .../shared/source_icon/source_icon.test.tsx | 6 --- .../shared/source_icon/source_icon.tsx | 10 +---- .../add_source/add_source_header.tsx | 7 +-- .../components/source_info_card.tsx | 1 - 42 files changed, 17 insertions(+), 117 deletions(-) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/crawler.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/drive.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/office365.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/one_drive.svg rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{sources_full_bleed => source_icons}/onedrive.svg (100%) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/people.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/service_now.svg rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{sources_full_bleed => source_icons}/servicenow.svg (100%) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_point.svg rename x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/{sources_full_bleed => source_icons}/sharepoint.svg (100%) delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg delete mode 100644 x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg index 23eff13915401..7aac36a6fe3c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/confluence.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/crawler.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/crawler.svg deleted file mode 100644 index d241989f1aff1..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/crawler.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg index f8f6415dea22b..cc07fbbc50877 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/custom.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/drive.svg deleted file mode 100644 index 40b65df3a1ce3..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/drive.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg index d16f293fde6dc..01e5a7735de12 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/dropbox.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg index c4b4176560d5b..aa9c3e5b45146 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg index ae068feb7133d..31fe60c6a63f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/gmail.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google.svg deleted file mode 100644 index 22630f533dcbf..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg index c684cecb71235..f3fe82cd3cd98 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/google_drive.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts index 347dee11670c9..e6a994d05f3ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts @@ -7,24 +7,18 @@ import box from './box.svg'; import confluence from './confluence.svg'; -import crawler from './crawler.svg'; import custom from './custom.svg'; -import drive from './drive.svg'; import dropbox from './dropbox.svg'; import github from './github.svg'; import gmail from './gmail.svg'; -import google from './google.svg'; import googleDrive from './google_drive.svg'; import jira from './jira.svg'; import jiraServer from './jira_server.svg'; import loadingSmall from './loading_small.svg'; -import office365 from './office365.svg'; -import oneDrive from './one_drive.svg'; -import outlook from './outlook.svg'; -import people from './people.svg'; +import oneDrive from './onedrive.svg'; import salesforce from './salesforce.svg'; -import serviceNow from './service_now.svg'; -import sharePoint from './share_point.svg'; +import serviceNow from './servicenow.svg'; +import sharePoint from './sharepoint.svg'; import slack from './slack.svg'; import zendesk from './zendesk.svg'; @@ -33,23 +27,17 @@ export const images = { confluence, confluenceCloud: confluence, confluenceServer: confluence, - crawler, custom, - drive, dropbox, github, githubEnterpriseServer: github, gmail, googleDrive, - google, jira, jiraServer, jiraCloud: jira, loadingSmall, - office365, oneDrive, - outlook, - people, salesforce, salesforceSandbox: salesforce, serviceNow, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg index 224bb822a581c..c12e55798d889 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg index 71750fb6e25a0..4dfd0fd910079 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/jira_server.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/office365.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/office365.svg deleted file mode 100644 index fdce5d02da3cd..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/office365.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/one_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/one_drive.svg deleted file mode 100644 index 1856e5e3ce1af..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/one_drive.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/onedrive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/onedrive.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/onedrive.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/onedrive.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg deleted file mode 100644 index 2680bc99cc367..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/outlook.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/people.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/people.svg deleted file mode 100644 index 4500c494c23b7..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/people.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg index 510c438a28195..ef6d552949424 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/salesforce.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/service_now.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/service_now.svg deleted file mode 100644 index 2d0c09db4e1c3..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/service_now.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/servicenow.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/servicenow.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/servicenow.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/servicenow.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg index f8d2ea1e634f6..39c40a65dfa70 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_circle.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_point.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_point.svg deleted file mode 100644 index 8724be9da88cf..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/share_point.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/sharepoint.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/sharepoint.svg similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/sharepoint.svg rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/sharepoint.svg diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg index 14dbd0289da84..8f6fc0c987eaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/slack.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg index f7bc1fda0c9ac..8afd143dd9a7c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/zendesk.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg deleted file mode 100644 index 827f8cf0a55ec..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg deleted file mode 100644 index 7aac36a6fe3c5..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/confluence.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg deleted file mode 100644 index cc07fbbc50877..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/custom.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg deleted file mode 100644 index 01e5a7735de12..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/dropbox.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg deleted file mode 100644 index aa9c3e5b45146..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/github.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg deleted file mode 100644 index 31fe60c6a63f9..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/gmail.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg deleted file mode 100644 index f3fe82cd3cd98..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/google_drive.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts deleted file mode 100644 index d9a0975abef7c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts +++ /dev/null @@ -1,45 +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 box from './box.svg'; -import confluence from './confluence.svg'; -import custom from './custom.svg'; -import dropbox from './dropbox.svg'; -import github from './github.svg'; -import gmail from './gmail.svg'; -import googleDrive from './google_drive.svg'; -import jira from './jira.svg'; -import jiraServer from './jira_server.svg'; -import oneDrive from './onedrive.svg'; -import salesforce from './salesforce.svg'; -import serviceNow from './servicenow.svg'; -import sharePoint from './sharepoint.svg'; -import slack from './slack.svg'; -import zendesk from './zendesk.svg'; - -export const imagesFull = { - box, - confluence, - confluenceCloud: confluence, - confluenceServer: confluence, - custom, - dropbox, - github, - githubEnterpriseServer: github, - gmail, - googleDrive, - jira, - jiraServer, - jiraCloud: jira, - oneDrive, - salesforce, - salesforceSandbox: salesforce, - serviceNow, - sharePoint, - slack, - zendesk, -} as { [key: string]: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg deleted file mode 100644 index c12e55798d889..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg deleted file mode 100644 index 4dfd0fd910079..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/jira_server.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg deleted file mode 100644 index ef6d552949424..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/salesforce.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg deleted file mode 100644 index 8f6fc0c987eaa..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/slack.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg deleted file mode 100644 index 8afd143dd9a7c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/zendesk.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx index 3bea6f224dc2b..ee079970a5ebb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx @@ -26,10 +26,4 @@ describe('SourceIcon', () => { expect(wrapper.find('.wrapped-icon')).toHaveLength(1); }); - - it('renders a full bleed icon', () => { - const wrapper = shallow(); - - expect(wrapper.find(EuiIcon).prop('type')).toEqual('test-file-stub'); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx index 03106dd7d8b8f..1d1462542a3f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.tsx @@ -14,14 +14,12 @@ import { EuiIcon, IconSize } from '@elastic/eui'; import './source_icon.scss'; import { images } from '../assets/source_icons'; -import { imagesFull } from '../assets/sources_full_bleed'; interface SourceIconProps { serviceType: string; name: string; className?: string; wrapped?: boolean; - fullBleed?: boolean; size?: IconSize; } @@ -30,16 +28,10 @@ export const SourceIcon: React.FC = ({ serviceType, className, wrapped, - fullBleed = false, size = 'xxl', }) => { const icon = ( - + ); return wrapped ? (
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx index bf472240d3c89..2ecb3c98565b7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx @@ -34,12 +34,7 @@ export const AddSourceHeader: React.FC = ({ responsive={false} > - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx index 765836191ff00..25c78afbe4e05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -41,7 +41,6 @@ export const SourceInfoCard: React.FC = ({ className="content-source-meta__icon" serviceType={sourceType} name={sourceType} - fullBleed size="l" /> From 5c352cace76d4f909d6823798a559cf7570281b1 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Wed, 10 Mar 2021 16:00:05 -0600 Subject: [PATCH 16/26] [Security Solution][Detections] Fix flaky indicator enrichment tests (#94241) * Make indicator enrichment tests order-independent Due to the fact that we use named queries to determine matches, and the fact that the order in which named queries are returned is undefined, we cannot guarantee a consistent ordering of enrichments if a given event matches multiple named queries. Because the ordering is not in itself important to enrichment, in order to assert the multi-match functionality we must make the assertions order independent. * PR feedback * Since we're only looping for side effects, prefer forEach to map for more idiomatic FP. --- .../tests/create_threat_matching.ts | 321 +++++++++--------- 1 file changed, 161 insertions(+), 160 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 59526fd5abb8f..a7925fa756693 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isEqual } from 'lodash'; import expect from '@kbn/expect'; import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; @@ -27,6 +28,18 @@ import { import { getCreateThreatMatchRulesSchemaMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock'; import { getThreatMatchingSchemaPartialMock } from '../../../../plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks'; +const format = (value: unknown): string => JSON.stringify(value, null, 2); + +// Asserts that each expected value is included in the subject, independent of +// ordering. Uses _.isEqual for value comparison. +const assertContains = (subject: unknown[], expected: unknown[]) => + expected.forEach((expectedValue) => + expect(subject.some((value) => isEqual(value, expectedValue))).to.eql( + true, + `expected ${format(subject)} to contain ${format(expectedValue)}` + ) + ); + // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertest'); @@ -35,8 +48,7 @@ export default ({ getService }: FtrProviderContext) => { /** * Specific api integration tests for threat matching rule type */ - // FLAKY: https://github.com/elastic/kibana/issues/93152 - describe.skip('create_threat_matching', () => { + describe('create_threat_matching', () => { describe('validation errors', () => { it('should give an error that the index must exist first if it does not exist before creating a rule', async () => { const { body } = await supertest @@ -383,40 +395,37 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsOpen.hits.hits.length).equal(1); const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source.threat); - expect(threats).to.eql([ + const [threat] = hits.map((hit) => hit._source.threat) as Array<{ indicator: unknown[] }>; + + assertContains(threat.indicator, [ { - indicator: [ - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - { - description: 'this should match auditbeat/hosts on ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978787', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'ip', - }, - provider: 'other_provider', - type: 'ip', - }, - ], + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978787', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'ip', + }, + provider: 'other_provider', + type: 'ip', }, ]); }); @@ -466,61 +475,57 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsOpen.hits.hits.length).equal(1); const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source.threat); + const [threat] = hits.map((hit) => hit._source.threat) as Array<{ indicator: unknown[] }>; - expect(threats).to.eql([ + assertContains(threat.indicator, [ { - indicator: [ - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - // We do not merge matched indicators during enrichment, so in - // certain circumstances a given indicator document could appear - // multiple times in an enriched alert (albeit with different - // threat.indicator.matched data). That's the case with the - // first and third indicators matched, here. - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: 57324, - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.port', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - { - description: 'this should match auditbeat/hosts on ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978787', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'ip', - }, - provider: 'other_provider', - type: 'ip', - }, - ], + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + // We do not merge matched indicators during enrichment, so in + // certain circumstances a given indicator document could appear + // multiple times in an enriched alert (albeit with different + // threat.indicator.matched data). That's the case with the + // first and third indicators matched, here. + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: 57324, + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.port', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + { + description: 'this should match auditbeat/hosts on ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978787', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'ip', + }, + provider: 'other_provider', + type: 'ip', }, ]); }); @@ -575,81 +580,77 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsOpen.hits.hits.length).equal(2); const { hits } = signalsOpen.hits; - const threats = hits.map((hit) => hit._source.threat); - expect(threats).to.eql([ + const threats = hits.map((hit) => hit._source.threat) as Array<{ indicator: unknown[] }>; + + assertContains(threats[0].indicator, [ { - indicator: [ - { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: 'url', - }, - provider: 'geenensp', - type: 'url', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - }, - ], + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: 'url', + }, + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, }, + ]); + + assertContains(threats[1].indicator, [ { - indicator: [ - { - description: "domain should match the auditbeat hosts' data's source.ip", - domain: '159.89.119.67', - first_seen: '2021-01-26T11:09:04.000Z', - matched: { - atomic: '159.89.119.67', - id: '978783', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'destination.ip', - type: 'url', - }, - provider: 'geenensp', - type: 'url', - url: { - full: 'http://159.89.119.67:59600/bin.sh', - scheme: 'http', - }, - }, - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: '45.115.45.3', - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.ip', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - { - description: 'this should match auditbeat/hosts on both port and ip', - first_seen: '2021-01-26T11:06:03.000Z', - ip: '45.115.45.3', - matched: { - atomic: 57324, - id: '978785', - index: 'filebeat-8.0.0-2021.01.26-000001', - field: 'source.port', - type: 'url', - }, - port: 57324, - provider: 'geenensp', - type: 'url', - }, - ], + description: "domain should match the auditbeat hosts' data's source.ip", + domain: '159.89.119.67', + first_seen: '2021-01-26T11:09:04.000Z', + matched: { + atomic: '159.89.119.67', + id: '978783', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'destination.ip', + type: 'url', + }, + provider: 'geenensp', + type: 'url', + url: { + full: 'http://159.89.119.67:59600/bin.sh', + scheme: 'http', + }, + }, + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: '45.115.45.3', + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.ip', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', + }, + { + description: 'this should match auditbeat/hosts on both port and ip', + first_seen: '2021-01-26T11:06:03.000Z', + ip: '45.115.45.3', + matched: { + atomic: 57324, + id: '978785', + index: 'filebeat-8.0.0-2021.01.26-000001', + field: 'source.port', + type: 'url', + }, + port: 57324, + provider: 'geenensp', + type: 'url', }, ]); }); From 26603620a4d423c7d31b2f6f8398973a7ac7205c Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 10 Mar 2021 16:16:46 -0600 Subject: [PATCH 17/26] [App Search] Role mappings migration part 1 (#94346) * Fix test suite name https://github.com/elastic/kibana/pull/94038/files#r590545670 * Move types out of AttributeSelector component to shared types * Fix random typo * Add routes and path generator util * Move constants to shared * Fix types in mock * Fix routes * Fix failing tests --- .../components/engines/engines_table.tsx | 6 ++--- .../components/role_mappings/utils.test.ts | 16 ++++++++++++++ .../components/role_mappings/utils.ts | 12 ++++++++++ .../applications/app_search/index.test.tsx | 2 +- .../public/applications/app_search/routes.ts | 5 ++++- .../utils/role/has_scoped_engines.test.ts | 2 +- .../shared/role_mapping/__mocks__/roles.ts | 6 +++-- .../role_mapping/attribute_selector.test.tsx | 4 +++- .../role_mapping/attribute_selector.tsx | 10 ++------- .../shared/role_mapping/constants.ts | 22 +++++++++++++++++++ .../public/applications/shared/types.ts | 10 ++++++++- .../views/role_mappings/constants.ts | 22 ------------------- .../views/role_mappings/role_mappings.tsx | 11 +++++----- .../role_mappings/role_mappings_logic.ts | 2 +- 14 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index e0c5823503445..3b9b6e6c6a778 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx @@ -54,7 +54,7 @@ export const EnginesTable: React.FC = ({ const { navigateToUrl } = useValues(KibanaLogic); const { hasPlatinumLicense } = useValues(LicensingLogic); - const generteEncodedEnginePath = (engineName: string) => + const generateEncodedEnginePath = (engineName: string) => generateEncodedPath(ENGINE_PATH, { engineName }); const sendEngineTableLinkClickTelemetry = () => sendAppSearchTelemetry({ @@ -71,7 +71,7 @@ export const EnginesTable: React.FC = ({ render: (name: string) => ( {name} @@ -159,7 +159,7 @@ export const EnginesTable: React.FC = ({ icon: 'eye', onClick: (engineDetails) => { sendEngineTableLinkClickTelemetry(); - navigateToUrl(generteEncodedEnginePath(engineDetails.name)); + navigateToUrl(generateEncodedEnginePath(engineDetails.name)); }, }, { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts new file mode 100644 index 0000000000000..e72f2b90758ac --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.test.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { generateRoleMappingPath } from './utils'; + +describe('generateRoleMappingPath', () => { + it('generates paths with roleId filled', () => { + const roleId = 'role123'; + + expect(generateRoleMappingPath(roleId)).toEqual(`/role_mappings/${roleId}`); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts new file mode 100644 index 0000000000000..109d3de1b86db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/utils.ts @@ -0,0 +1,12 @@ +/* + * 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 { ROLE_MAPPING_PATH } from '../../routes'; +import { generateEncodedPath } from '../../utils/encode_path_params'; + +export const generateRoleMappingPath = (roleId: string) => + generateEncodedPath(ROLE_MAPPING_PATH, { roleId }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 6827f4f5e827d..4da71ec9a135b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -156,7 +156,7 @@ describe('AppSearchNav', () => { const wrapper = shallow(); expect(wrapper.find(SideNavLink).last().prop('to')).toEqual( - 'http://localhost:3002/as#/role-mappings' + 'http://localhost:3002/as/role_mappings' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 907a27c8660d2..9ab67601969d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -14,7 +14,10 @@ export const SETUP_GUIDE_PATH = '/setup_guide'; export const LIBRARY_PATH = '/library'; export const SETTINGS_PATH = '/settings/account'; export const CREDENTIALS_PATH = '/credentials'; -export const ROLE_MAPPINGS_PATH = '#/role-mappings'; // This page seems to 404 if the # isn't included + +export const ROLE_MAPPINGS_PATH = '/role_mappings'; +export const ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/:roleId`; +export const ROLE_MAPPING_NEW_PATH = `${ROLE_MAPPINGS_PATH}/new`; export const ENGINES_PATH = '/engines'; export const ENGINE_CREATION_PATH = '/engine_creation'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/has_scoped_engines.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/has_scoped_engines.test.ts index 0918d2025f732..ecbf885ac3b5c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/has_scoped_engines.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/has_scoped_engines.test.ts @@ -7,7 +7,7 @@ import { roleHasScopedEngines } from './'; -describe('roleHasScopedEngines()', () => { +describe('roleHasScopedEngines', () => { it('returns false for owner and admin roles', () => { expect(roleHasScopedEngines('owner')).toEqual(false); expect(roleHasScopedEngines('admin')).toEqual(false); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts index 6e9c867b15679..1576fa178cfa9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/__mocks__/roles.ts @@ -5,9 +5,11 @@ * 2.0. */ +import { AttributeName } from '../../types'; + export const asRoleMapping = { id: null, - attributeName: 'role', + attributeName: 'role' as AttributeName, attributeValue: ['superuser'], authProvider: ['*'], roleType: 'owner', @@ -23,7 +25,7 @@ export const asRoleMapping = { export const wsRoleMapping = { id: '602d4ba85foobarbaz123', - attributeName: 'username', + attributeName: 'username' as AttributeName, attributeValue: 'user', authProvider: ['*', 'other_auth'], roleType: 'admin', diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx index bc31732527b0e..b5d1ebb899ba1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.test.tsx @@ -11,7 +11,9 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiComboBox, EuiFieldText } from '@elastic/eui'; -import { AttributeSelector, AttributeName } from './attribute_selector'; +import { AttributeName } from '../types'; + +import { AttributeSelector } from './attribute_selector'; import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL } from './constants'; const handleAttributeSelectorChange = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx index d19107b534fb7..48d1447e9bd0f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/attribute_selector.tsx @@ -20,6 +20,8 @@ import { EuiTitle, } from '@elastic/eui'; +import { AttributeName, AttributeExamples } from '../types'; + import { ANY_AUTH_PROVIDER, ANY_AUTH_PROVIDER_OPTION_LABEL, @@ -31,8 +33,6 @@ import { ATTRIBUTE_VALUE_LABEL, } from './constants'; -export type AttributeName = keyof AttributeExamples | 'role'; - interface Props { attributeName: AttributeName; attributeValue?: string; @@ -47,12 +47,6 @@ interface Props { handleAuthProviderChange?(value: string[]): void; } -interface AttributeExamples { - username: string; - email: string; - metadata: string; -} - interface ParentOption extends EuiComboBoxOptionOption { label: string; options: ChildOption[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index 1fbbc172dcf69..9eae2ce06efa2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -107,3 +107,25 @@ export const MANAGE_ROLE_MAPPING_BUTTON = i18n.translate( defaultMessage: 'Manage', } ); + +export const ROLE_MAPPINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingsTitle', + { + defaultMessage: 'Users & roles', + } +); + +export const EMPTY_ROLE_MAPPINGS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.emptyRoleMappingsTitle', + { + defaultMessage: 'No role mappings yet', + } +); + +export const ROLE_MAPPINGS_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.roleMappingsDescription', + { + defaultMessage: + 'Define role mappings for elasticsearch-native and elasticsearch-saml authentication.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 16a9e3b54aea5..e026e2f592e75 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -51,9 +51,17 @@ export interface RoleRules { metadata?: string; } +export interface AttributeExamples { + username: string; + email: string; + metadata: string; +} + +export type AttributeName = keyof AttributeExamples | 'role'; + export interface RoleMapping { id: string; - attributeName: string; + attributeName: AttributeName; attributeValue: string; authProvider: string[]; roleType: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts index a4b4e4a1bfd29..a44144666d139 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/constants.ts @@ -59,13 +59,6 @@ export const GROUP_ASSIGNMENT_ALL_GROUPS_LABEL = i18n.translate( } ); -export const EMPTY_ROLE_MAPPINGS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsTitle', - { - defaultMessage: 'No role mappings yet', - } -); - export const EMPTY_ROLE_MAPPINGS_BODY = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.roleMapping.emptyRoleMappingsBody', { @@ -80,18 +73,3 @@ export const ROLE_MAPPINGS_TABLE_HEADER = i18n.translate( defaultMessage: 'Group Access', } ); - -export const ROLE_MAPPINGS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsTitle', - { - defaultMessage: 'Users & roles', - } -); - -export const ROLE_MAPPINGS_DESCRIPTION = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.roleMapping.roleMappingsDescription', - { - defaultMessage: - 'Define role mappings for elasticsearch-native and elasticsearch-saml authentication.', - } -); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx index e47b2646459df..842c59e683f06 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings.tsx @@ -14,16 +14,15 @@ import { EuiEmptyPrompt } from '@elastic/eui'; import { FlashMessages } from '../../../shared/flash_messages'; import { Loading } from '../../../shared/loading'; import { AddRoleMappingButton, RoleMappingsTable } from '../../../shared/role_mapping'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { getRoleMappingPath, ROLE_MAPPING_NEW_PATH } from '../../routes'; - import { EMPTY_ROLE_MAPPINGS_TITLE, - EMPTY_ROLE_MAPPINGS_BODY, - ROLE_MAPPINGS_TABLE_HEADER, ROLE_MAPPINGS_TITLE, ROLE_MAPPINGS_DESCRIPTION, -} from './constants'; +} from '../../../shared/role_mapping/constants'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { getRoleMappingPath, ROLE_MAPPING_NEW_PATH } from '../../routes'; + +import { EMPTY_ROLE_MAPPINGS_BODY, ROLE_MAPPINGS_TABLE_HEADER } from './constants'; import { RoleMappingsLogic } from './role_mappings_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts index 6fc3867d7ab1e..b43bda3bb228e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.ts @@ -10,8 +10,8 @@ import { kea, MakeLogicType } from 'kea'; import { clearFlashMessages, flashAPIErrors } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; -import { AttributeName } from '../../../shared/role_mapping/attribute_selector'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { AttributeName } from '../../../shared/types'; import { ROLE_MAPPINGS_PATH } from '../../routes'; import { RoleGroup, WSRoleMapping, Role } from '../../types'; From 9aeb9f4e4cd7dda6eb730edbd4bbe184266e872c Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 10 Mar 2021 15:41:52 -0700 Subject: [PATCH 18/26] skip another suite blocking es promotion (#94367) --- .../security_and_spaces/tests/finalize_signals_migrations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts index 0aac596cc3adb..89228fa4f239d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts @@ -47,7 +47,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('Finalizing signals migrations', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/94367 + describe.skip('Finalizing signals migrations', () => { let legacySignalsIndexName: string; let outdatedSignalsIndexName: string; let createdMigrations: CreateResponse[]; From 14c32cbd6c24aea473f9819f4628206dbd1ab5c7 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Wed, 10 Mar 2021 17:17:55 -0700 Subject: [PATCH 19/26] [Security Solution] Eliminates a redundant external link icon (#94194) ## [Security Solution] Eliminates a redundant external link icon - Fixes an issue where [a redundant external link icon](https://github.com/elastic/kibana/issues/89084) was rendered next to port numbers Per the [EuiLink documentation](https://elastic.github.io/eui/#/navigation/link), it's no longer necessary to render our own icon, because `EuiLink` will automatically display one when `target="_blank"` is passed as a prop to the link. - Updates the existing link icon unit test such that it asserts a specific icon count to catch any regressions ### Before before ### After after ### Desk testing Desk tested in: - Chrome `89.0.4389.82` - Firefox `86.0` - Safari `14.0.3` --- .../external_link_icon/index.test.tsx | 76 ------------------- .../components/external_link_icon/index.tsx | 48 ------------ .../common/components/links/index.test.tsx | 16 ++-- .../public/common/components/links/index.tsx | 37 +++++---- .../port/__snapshots__/index.test.tsx.snap | 1 - .../network/components/port/index.test.tsx | 4 +- .../public/network/components/port/index.tsx | 2 - .../certificate_fingerprint/index.tsx | 2 - .../components/ja3_fingerprint/index.tsx | 2 - .../row_renderers_browser/catalog/index.tsx | 2 - .../body/renderers/suricata/suricata_refs.tsx | 2 - 11 files changed, 34 insertions(+), 158 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/external_link_icon/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/external_link_icon/index.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.test.tsx deleted file mode 100644 index 65da5423d19e8..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.test.tsx +++ /dev/null @@ -1,76 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../mock'; - -import { ExternalLinkIcon } from '.'; - -describe('Duration', () => { - test('it renders expected icon type when the leftMargin prop is not specified', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first().props().type).toEqual( - 'popout' - ); - }); - - test('it renders expected icon type when the leftMargin prop is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first().props().type).toEqual( - 'popout' - ); - }); - - test('it applies a margin-left style when the leftMargin prop is true', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first()).toHaveStyleRule( - 'margin-left', - '5px' - ); - }); - - test('it does NOT apply a margin-left style when the leftMargin prop is false', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first()).not.toHaveStyleRule( - 'margin-left' - ); - }); - - test('it renders expected icon type when the leftMargin prop is false', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="external-link-icon"]').first().props().type).toEqual( - 'popout' - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.tsx b/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.tsx deleted file mode 100644 index 825ee8a33a8a5..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/external_link_icon/index.tsx +++ /dev/null @@ -1,48 +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 { EuiIcon } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -const LinkIcon = styled(EuiIcon)` - position: relative; - top: -2px; -`; - -LinkIcon.displayName = 'LinkIcon'; - -const LinkIconWithMargin = styled(LinkIcon)` - margin-left: 5px; -`; - -LinkIconWithMargin.displayName = 'LinkIconWithMargin'; - -const color = 'subdued'; -const iconSize = 's'; -const iconType = 'popout'; - -/** - * Renders an icon that indicates following the hyperlink will navigate to - * content external to the app - */ -export const ExternalLinkIcon = React.memo<{ - leftMargin?: boolean; -}>(({ leftMargin = true }) => - leftMargin ? ( - - ) : ( - - ) -); - -ExternalLinkIcon.displayName = 'ExternalLinkIcon'; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index 864e55b3e4a45..cd19eb5a27d7b 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { mount, shallow, ShallowWrapper } from 'enzyme'; +import { mount, shallow, ReactWrapper, ShallowWrapper } from 'enzyme'; import React from 'react'; import { removeExternalLinkText } from '../../../../common/test_utils'; import { mountWithIntl } from '@kbn/test/jest'; @@ -121,11 +121,11 @@ describe('Custom Links', () => { describe('External Link', () => { const mockLink = 'https://www.virustotal.com/gui/search/'; const mockLinkName = 'Link'; - let wrapper: ShallowWrapper; + let wrapper: ReactWrapper | ShallowWrapper; describe('render', () => { beforeAll(() => { - wrapper = shallow( + wrapper = mount( {mockLinkName} @@ -137,11 +137,13 @@ describe('Custom Links', () => { }); test('it renders ExternalLinkIcon', () => { - expect(wrapper.find('[data-test-subj="externalLinkIcon"]').exists()).toBeTruthy(); + expect(wrapper.find('span [data-euiicon-type="popout"]').length).toBe(1); }); test('it renders correct url', () => { - expect(wrapper.find('[data-test-subj="externalLink"]').prop('href')).toEqual(mockLink); + expect(wrapper.find('[data-test-subj="externalLink"]').first().prop('href')).toEqual( + mockLink + ); }); test('it renders comma if id is given', () => { @@ -435,14 +437,14 @@ describe('Custom Links', () => { test('it renders correct number of external icons by default', () => { const wrapper = mountWithIntl(); - expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(5); + expect(wrapper.find('span [data-euiicon-type="popout"]')).toHaveLength(5); }); test('it renders correct number of external icons', () => { const wrapper = mountWithIntl( ); - expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(1); + expect(wrapper.find('span [data-euiicon-type="popout"]')).toHaveLength(1); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 8e2f57a1a597c..a02cc8bf76bcc 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -39,7 +39,6 @@ import { } from '../../../../common/search_strategy/security_solution/network'; import { useUiSetting$, useKibana } from '../../lib/kibana'; import { isUrlInvalid } from '../../utils/validators'; -import { ExternalLinkIcon } from '../external_link_icon'; import * as i18n from './translations'; import { SecurityPageName } from '../../../app/types'; @@ -54,6 +53,13 @@ export const LinkAnchor: React.FC = ({ children, ...props }) => ( {children} ); +export const PortContainer = styled.div` + & svg { + position: relative; + top: -1px; + } +`; + // Internal Links const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; @@ -112,11 +118,12 @@ export const ExternalLink = React.memo<{ const inAllowlist = allowedUrlSchemes.some((scheme) => url.indexOf(scheme) === 0); return url && inAllowlist && !isUrlInvalid(url) && children ? ( - - {children} - + <> + + {children} + {!isNil(idx) && idx < lastIndexToShow && } - + ) : null; } @@ -229,15 +236,17 @@ export const PortOrServiceNameLink = React.memo<{ children?: React.ReactNode; portOrServiceName: number | string; }>(({ children, portOrServiceName }) => ( - - {children ? children : portOrServiceName} - + + + {children ? children : portOrServiceName} + + )); PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; diff --git a/x-pack/plugins/security_solution/public/network/components/port/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/port/__snapshots__/index.test.tsx.snap index ecdaee0ab2d93..f84e858d20573 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/port/__snapshots__/index.test.tsx.snap @@ -11,6 +11,5 @@ exports[`Port renders correctly against snapshot 1`] = ` - `; diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx index 47dfffefe091c..9c7a0833b24bb 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.test.tsx @@ -60,13 +60,13 @@ describe('Port', () => { ); }); - test('it renders an external link', () => { + test('it renders only one external link icon', () => { const wrapper = mount( ); - expect(wrapper.find('[data-test-subj="external-link-icon"]').first().exists()).toBe(true); + expect(wrapper.find('span [data-euiicon-type="popout"]').length).toBe(1); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/port/index.tsx b/x-pack/plugins/security_solution/public/network/components/port/index.tsx index 0744ca175aa38..8ee1616d4c77b 100644 --- a/x-pack/plugins/security_solution/public/network/components/port/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/port/index.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { DefaultDraggable } from '../../../common/components/draggables'; import { getEmptyValue } from '../../../common/components/empty_value'; -import { ExternalLinkIcon } from '../../../common/components/external_link_icon'; import { PortOrServiceNameLink } from '../../../common/components/links'; export const CLIENT_PORT_FIELD_NAME = 'client.port'; @@ -40,7 +39,6 @@ export const Port = React.memo<{ value={value} > - )); diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx index d850204284bd0..29775067478a5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.tsx @@ -10,7 +10,6 @@ import React from 'react'; import styled from 'styled-components'; import { DraggableBadge } from '../../../common/components/draggables'; -import { ExternalLinkIcon } from '../../../common/components/external_link_icon'; import { CertificateFingerprintLink } from '../../../common/components/links'; import * as i18n from './translations'; @@ -61,7 +60,6 @@ export const CertificateFingerprint = React.memo<{ {certificateType === 'client' ? i18n.CLIENT_CERT : i18n.SERVER_CERT} - ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx index df8c68df483c5..d73130417566f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.tsx @@ -9,7 +9,6 @@ import React from 'react'; import styled from 'styled-components'; import { DraggableBadge } from '../../../common/components/draggables'; -import { ExternalLinkIcon } from '../../../common/components/external_link_icon'; import { Ja3FingerprintLink } from '../../../common/components/links'; import * as i18n from './translations'; @@ -45,7 +44,6 @@ export const Ja3Fingerprint = React.memo<{ {i18n.JA3_FINGERPRINT_LABEL} - )); diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx index 65974a10c49c2..283a239acad24 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/catalog/index.tsx @@ -7,7 +7,6 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { ExternalLinkIcon } from '../../../../common/components/external_link_icon'; import { RowRendererId } from '../../../../../common/types/timeline'; import { @@ -37,7 +36,6 @@ const Link = ({ children, url }: { children: React.ReactNode; url: string }) => data-test-subj="externalLink" > {children} - ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx index 0bbd86479c226..96e6b916a3e11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx @@ -9,7 +9,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon'; import { getLinksFromSignature } from './suricata_links'; const LinkEuiFlexItem = styled(EuiFlexItem)` @@ -27,7 +26,6 @@ export const SuricataRefs = React.memo<{ signatureId: number }>(({ signatureId } {link} - ))} From ad0517a905c823e5f639d052c4740ffb5a01c38a Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 10 Mar 2021 18:19:47 -0700 Subject: [PATCH 20/26] skip another suite blocking es promotion (#94367) --- .../security_and_spaces/tests/delete_signals_migrations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts index d54b4525459e8..cdb5035c06155 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/delete_signals_migrations.ts @@ -35,7 +35,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('deleting signals migrations', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/94367 + describe.skip('deleting signals migrations', () => { let outdatedSignalsIndexName: string; let createdMigration: CreateResponse; let finalizedMigration: FinalizeResponse; From 92307bfe294ed3f0451923921a32fe7236b1fe27 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Thu, 11 Mar 2021 10:11:53 +0100 Subject: [PATCH 21/26] [ML] Functional tests - stabilize slider value selection (#94313) This PR stabilizes the slider value selection during ML functional tests. --- x-pack/test/functional/services/ml/common_ui.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 727f6493910ff..70e3d7c1b9b15 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -167,10 +167,10 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte async setSliderValue(testDataSubj: string, value: number) { const slider = await testSubjects.find(testDataSubj); - let currentValue = await slider.getAttribute('value'); - let currentDiff = +currentValue - +value; - await retry.tryForTime(60 * 1000, async () => { + const currentValue = await slider.getAttribute('value'); + const currentDiff = +currentValue - +value; + if (currentDiff === 0) { return true; } else { @@ -189,20 +189,13 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte } await retry.tryForTime(1000, async () => { const newValue = await slider.getAttribute('value'); - if (newValue !== currentValue) { - currentValue = newValue; - currentDiff = +currentValue - +value; - return true; - } else { + if (newValue === currentValue) { throw new Error(`slider value should have changed, but is still ${currentValue}`); } }); - - throw new Error(`slider value should be '${value}' (got '${currentValue}')`); + await this.assertSliderValue(testDataSubj, value); } }); - - await this.assertSliderValue(testDataSubj, value); }, async assertSliderValue(testDataSubj: string, expectedValue: number) { From 77fe83b1a62954579e91eb03b6a5e8e3309dfe05 Mon Sep 17 00:00:00 2001 From: Daniil Date: Thu, 11 Mar 2021 12:15:19 +0300 Subject: [PATCH 22/26] [TSVB] Type public code. Step 1 (#93231) * Remove request facade and update search strategies * Use typescript * Type files * Update structure * Update tests * Type annotations * Fix type for infra * Type editor_controller * Type vis_editor * Type vis_picker * Fix types * Type panel_config * Fix vis data type * Enhance types * Remove generics * Use constant * Update docs * Use empty object as default data * Fix merge conflict --- .../application/components/index_pattern.js | 6 +- .../application/components/panel_config.js | 85 ------- .../application/components/panel_config.tsx | 79 +++++++ .../components/panel_config/index.ts | 30 +++ .../components/timeseries_visualization.tsx | 2 +- .../{vis_editor.js => vis_editor.tsx} | 207 +++++++++--------- .../components/vis_editor_lazy.tsx | 4 +- .../application/components/vis_picker.js | 98 --------- .../application/components/vis_picker.tsx | 70 ++++++ .../application/components/vis_types/index.ts | 2 +- ..._context.js => form_validation_context.ts} | 0 ...is_data_context.js => vis_data_context.ts} | 3 +- ...or_controller.js => editor_controller.tsx} | 33 ++- .../public/application/index.ts | 1 - .../public/application/lib/fetch_fields.ts | 6 +- .../vis_type_timeseries/public/metrics_fn.ts | 5 +- .../public/request_handler.ts | 2 +- .../public/timeseries_vis_renderer.tsx | 3 +- .../vis_type_timeseries/public/to_ast.ts | 3 +- .../vis_type_timeseries/public/types.ts | 3 + src/plugins/visualizations/public/vis.ts | 12 +- .../visualize/public/application/types.ts | 5 +- .../visualize/public/vis_editors_registry.ts | 4 +- 23 files changed, 336 insertions(+), 327 deletions(-) delete mode 100644 src/plugins/vis_type_timeseries/public/application/components/panel_config.js create mode 100644 src/plugins/vis_type_timeseries/public/application/components/panel_config.tsx create mode 100644 src/plugins/vis_type_timeseries/public/application/components/panel_config/index.ts rename src/plugins/vis_type_timeseries/public/application/components/{vis_editor.js => vis_editor.tsx} (50%) delete mode 100644 src/plugins/vis_type_timeseries/public/application/components/vis_picker.js create mode 100644 src/plugins/vis_type_timeseries/public/application/components/vis_picker.tsx rename src/plugins/vis_type_timeseries/public/application/contexts/{form_validation_context.js => form_validation_context.ts} (100%) rename src/plugins/vis_type_timeseries/public/application/contexts/{vis_data_context.js => vis_data_context.ts} (68%) rename src/plugins/vis_type_timeseries/public/application/{editor_controller.js => editor_controller.tsx} (59%) diff --git a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js index d36cea80bffff..61198518dc333 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js +++ b/src/plugins/vis_type_timeseries/public/application/components/index_pattern.js @@ -8,7 +8,7 @@ import { get } from 'lodash'; import PropTypes from 'prop-types'; -import React, { useContext, useCallback } from 'react'; +import React, { useContext, useCallback, useEffect } from 'react'; import { htmlIdGenerator, EuiFieldText, @@ -123,7 +123,9 @@ export const IndexPattern = ({ ); const isTimeSeries = model.type === PANEL_TYPES.TIMESERIES; - updateControlValidity(intervalName, intervalValidation.isValid); + useEffect(() => { + updateControlValidity(intervalName, intervalValidation.isValid); + }, [intervalName, intervalValidation.isValid, updateControlValidity]); return (
diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config.js deleted file mode 100644 index 2e6e97f868fd9..0000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config.js +++ /dev/null @@ -1,85 +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 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 PropTypes from 'prop-types'; -import React, { useState, useEffect } from 'react'; -import { TimeseriesPanelConfig as timeseries } from './panel_config/timeseries'; -import { MetricPanelConfig as metric } from './panel_config/metric'; -import { TopNPanelConfig as topN } from './panel_config/top_n'; -import { TablePanelConfig as table } from './panel_config/table'; -import { GaugePanelConfig as gauge } from './panel_config/gauge'; -import { MarkdownPanelConfig as markdown } from './panel_config/markdown'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { FormValidationContext } from '../contexts/form_validation_context'; -import { VisDataContext } from '../contexts/vis_data_context'; - -const types = { - timeseries, - table, - metric, - top_n: topN, - gauge, - markdown, -}; - -const checkModelValidity = (validationResults) => - Boolean(Object.values(validationResults).every((isValid) => isValid)); - -export function PanelConfig(props) { - const { model } = props; - const Component = types[model.type]; - const [formValidationResults] = useState({}); - const [visData, setVisData] = useState({}); - - useEffect(() => { - model.isModelInvalid = !checkModelValidity(formValidationResults); - }); - - useEffect(() => { - const visDataSubscription = props.visData$.subscribe((visData = {}) => setVisData(visData)); - - return function cleanup() { - visDataSubscription.unsubscribe(); - }; - }, [model.id, props.visData$]); - - const updateControlValidity = (controlKey, isControlValid) => { - formValidationResults[controlKey] = isControlValid; - }; - - if (Component) { - return ( - - -
- -
-
-
- ); - } - - return ( -
- -
- ); -} - -PanelConfig.propTypes = { - fields: PropTypes.object, - model: PropTypes.object, - onChange: PropTypes.func, - visData$: PropTypes.object, - getConfig: PropTypes.func, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config.tsx new file mode 100644 index 0000000000000..b1eed37986e16 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config.tsx @@ -0,0 +1,79 @@ +/* + * 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 React, { useState, useEffect, useCallback, useRef } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Observable } from 'rxjs'; + +import { IUiSettingsClient } from 'kibana/public'; +import { TimeseriesVisData } from '../../../common/types'; +import { FormValidationContext } from '../contexts/form_validation_context'; +import { VisDataContext } from '../contexts/vis_data_context'; +import { panelConfigTypes } from './panel_config/index'; +import { TimeseriesVisParams } from '../../types'; +import { VisFields } from '../lib/fetch_fields'; + +interface FormValidationResults { + [key: string]: boolean; +} + +interface PanelConfigProps { + fields?: VisFields; + model: TimeseriesVisParams; + visData$: Observable; + getConfig: IUiSettingsClient['get']; + onChange: (partialModel: Partial) => void; +} + +const checkModelValidity = (validationResults: FormValidationResults) => + Object.values(validationResults).every((isValid) => isValid); + +export function PanelConfig(props: PanelConfigProps) { + const { model, onChange } = props; + const Component = panelConfigTypes[model.type]; + const formValidationResults = useRef({}); + const [visData, setVisData] = useState({} as TimeseriesVisData); + + useEffect(() => { + const visDataSubscription = props.visData$.subscribe((data = {} as TimeseriesVisData) => + setVisData(data) + ); + + return () => visDataSubscription.unsubscribe(); + }, [model.id, props.visData$]); + + const updateControlValidity = useCallback( + (controlKey: string, isControlValid: boolean) => { + formValidationResults.current[controlKey] = isControlValid; + onChange({ isModelInvalid: !checkModelValidity(formValidationResults.current) }); + }, + [onChange] + ); + + if (Component) { + return ( + + +
+ +
+
+
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/index.ts b/src/plugins/vis_type_timeseries/public/application/components/panel_config/index.ts new file mode 100644 index 0000000000000..f50ecca1894aa --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +// these are not typed yet +// @ts-expect-error +import { TimeseriesPanelConfig as timeseries } from './timeseries'; +// @ts-expect-error +import { MetricPanelConfig as metric } from './metric'; +// @ts-expect-error +import { TopNPanelConfig as topN } from './top_n'; +// @ts-expect-error +import { TablePanelConfig as table } from './table'; +// @ts-expect-error +import { GaugePanelConfig as gauge } from './gauge'; +// @ts-expect-error +import { MarkdownPanelConfig as markdown } from './markdown'; + +export const panelConfigTypes = { + timeseries, + table, + metric, + top_n: topN, + gauge, + markdown, +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx index fe09ae253f8a8..9c8944a2e6e62 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -15,7 +15,7 @@ import { PersistedState } from 'src/plugins/visualizations/public'; // @ts-expect-error import { ErrorComponent } from './error'; import { TimeseriesVisTypes } from './vis_types'; -import { TimeseriesVisParams } from '../../metrics_fn'; +import { TimeseriesVisParams } from '../../types'; import { TimeseriesVisData } from '../../../common/types'; interface TimeseriesVisualizationProps { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx similarity index 50% rename from src/plugins/vis_type_timeseries/public/application/components/vis_editor.js rename to src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx index 11586628ea005..ffef437358c3d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.tsx @@ -6,50 +6,74 @@ * Side Public License, v 1. */ -import PropTypes from 'prop-types'; import React, { Component } from 'react'; import * as Rx from 'rxjs'; import { share } from 'rxjs/operators'; import { isEqual, isEmpty, debounce } from 'lodash'; +import { EventEmitter } from 'events'; + +import { IUiSettingsClient } from 'kibana/public'; +import { TimeRange } from 'src/plugins/data/public'; +import { + PersistedState, + Vis, + VisualizeEmbeddableContract, +} from 'src/plugins/visualizations/public'; +import { TimeseriesVisData } from 'src/plugins/vis_type_timeseries/common/types'; +import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; +import { Storage } from '../../../../../plugins/kibana_utils/public'; + +// @ts-expect-error import { VisEditorVisualization } from './vis_editor_visualization'; -import { VisPicker } from './vis_picker'; import { PanelConfig } from './panel_config'; -import { fetchFields } from '../lib/fetch_fields'; import { extractIndexPatterns } from '../../../common/extract_index_patterns'; -import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services'; - -import { CoreStartContextProvider } from '../contexts/query_input_bar_context'; -import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; -import { Storage } from '../../../../../plugins/kibana_utils/public'; +import { VisPicker } from './vis_picker'; +import { fetchFields, VisFields } from '../lib/fetch_fields'; +import { getDataStart, getCoreStart } from '../../services'; +import { TimeseriesVisParams } from '../../types'; const VIS_STATE_DEBOUNCE_DELAY = 200; const APP_NAME = 'VisEditor'; -export class VisEditor extends Component { - constructor(props) { - super(props); - this.localStorage = new Storage(window.localStorage); - this.state = {}; +export interface TimeseriesEditorProps { + config: IUiSettingsClient; + embeddableHandler: VisualizeEmbeddableContract; + eventEmitter: EventEmitter; + timeRange: TimeRange; + uiState: PersistedState; + vis: Vis; +} - this.visDataSubject = new Rx.BehaviorSubject(this.props.visData); - this.visData$ = this.visDataSubject.asObservable().pipe(share()); +interface TimeseriesEditorState { + autoApply: boolean; + dirty: boolean; + extractedIndexPatterns: string[]; + model: TimeseriesVisParams; + visFields?: VisFields; +} + +export class VisEditor extends Component { + private abortControllerFetchFields?: AbortController; + private localStorage: Storage; + private visDataSubject: Rx.BehaviorSubject; + private visData$: Rx.Observable; - // In new_platform, this context should be populated with - // core dependencies required by React components downstream. - this.coreContext = { - appName: APP_NAME, - uiSettings: getUISettings(), - savedObjectsClient: getSavedObjectsClient(), - store: this.localStorage, + constructor(props: TimeseriesEditorProps) { + super(props); + this.localStorage = new Storage(window.localStorage); + this.state = { + autoApply: true, + dirty: false, + model: this.props.vis.params, + extractedIndexPatterns: [''], }; - } - get uiState() { - return this.props.vis.uiState; + this.visDataSubject = new Rx.BehaviorSubject(undefined); + this.visData$ = this.visDataSubject.asObservable().pipe(share()); } - getConfig = (...args) => { - return this.props.config.get(...args); + getConfig = (key: string) => { + return this.props.config.get(key); }; updateVisState = debounce(() => { @@ -73,16 +97,14 @@ export class VisEditor extends Component { } }, VIS_STATE_DEBOUNCE_DELAY); - abortableFetchFields = (extractedIndexPatterns) => { - if (this.abortControllerFetchFields) { - this.abortControllerFetchFields.abort(); - } + abortableFetchFields = (extractedIndexPatterns: string[]) => { + this.abortControllerFetchFields?.abort(); this.abortControllerFetchFields = new AbortController(); return fetchFields(extractedIndexPatterns, this.abortControllerFetchFields.signal); }; - handleChange = (partialModel) => { + handleChange = (partialModel: Partial) => { if (isEmpty(partialModel)) { return; } @@ -117,64 +139,62 @@ export class VisEditor extends Component { this.setState({ dirty: false }); }; - handleAutoApplyToggle = (event) => { + handleAutoApplyToggle = (event: React.ChangeEvent) => { this.setState({ autoApply: event.target.checked }); }; - onDataChange = ({ visData }) => { + onDataChange = ({ visData }: { visData: TimeseriesVisData }) => { this.visDataSubject.next(visData); }; render() { - const { model } = this.state; - - if (model) { - //TODO: Remove CoreStartContextProvider, KibanaContextProvider should be raised to the top of the plugin. - return ( - -
-
- -
- +
+
+ +
+ +
+ -
- - - -
- - ); - } - - return null; +
+ + ); } componentDidMount() { @@ -182,11 +202,12 @@ export class VisEditor extends Component { dataStart.indexPatterns.getDefault().then(async (index) => { const defaultIndexTitle = index?.title ?? ''; - const indexPatterns = extractIndexPatterns(this.props.visParams, defaultIndexTitle); + const indexPatterns = extractIndexPatterns(this.props.vis.params, defaultIndexTitle); + const visFields = await fetchFields(indexPatterns); - this.setState({ + this.setState((state) => ({ model: { - ...this.props.visParams, + ...state.model, /** @legacy * please use IndexPatterns service instead * **/ @@ -196,11 +217,8 @@ export class VisEditor extends Component { * **/ default_timefield: index?.timeFieldName ?? '', }, - dirty: false, - autoApply: true, - visFields: await fetchFields(indexPatterns), - extractedIndexPatterns: [''], - }); + visFields, + })); }); this.props.eventEmitter.on('updateEditor', this.updateModel); @@ -212,19 +230,6 @@ export class VisEditor extends Component { } } -VisEditor.defaultProps = { - visData: {}, -}; - -VisEditor.propTypes = { - vis: PropTypes.object, - visData: PropTypes.object, - renderComplete: PropTypes.func, - config: PropTypes.object, - timeRange: PropTypes.object, - appState: PropTypes.object, -}; - // default export required for React.Lazy // eslint-disable-next-line import/no-default-export export { VisEditor as default }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx index 2cbd88cefcf0a..8b54349c495f4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_lazy.tsx @@ -8,11 +8,11 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; +import type { TimeseriesEditorProps } from './vis_editor'; -// @ts-ignore const VisEditorComponent = lazy(() => import('./vis_editor')); -export const VisEditor = (props: any) => ( +export const VisEditor = (props: TimeseriesEditorProps) => ( }> diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js deleted file mode 100644 index fab1de45156cc..0000000000000 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.js +++ /dev/null @@ -1,98 +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 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 PropTypes from 'prop-types'; -import React from 'react'; -import { EuiTabs, EuiTab } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; -import { PANEL_TYPES } from '../../../common/panel_types'; - -function VisPickerItem(props) { - const { label, type, selected } = props; - const itemClassName = 'tvbVisPickerItem'; - - return ( - props.onClick(type)} - data-test-subj={`${type}TsvbTypeBtn`} - > - {label} - - ); -} - -VisPickerItem.propTypes = { - label: PropTypes.string, - onClick: PropTypes.func, - type: PropTypes.string, - selected: PropTypes.bool, -}; - -export const VisPicker = injectI18n(function (props) { - const handleChange = (type) => { - props.onChange({ type }); - }; - - const { model, intl } = props; - const tabs = [ - { - type: PANEL_TYPES.TIMESERIES, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.timeSeriesLabel', - defaultMessage: 'Time Series', - }), - }, - { - type: PANEL_TYPES.METRIC, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.metricLabel', - defaultMessage: 'Metric', - }), - }, - { - type: PANEL_TYPES.TOP_N, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.topNLabel', - defaultMessage: 'Top N', - }), - }, - { - type: PANEL_TYPES.GAUGE, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.gaugeLabel', - defaultMessage: 'Gauge', - }), - }, - { type: PANEL_TYPES.MARKDOWN, label: 'Markdown' }, - { - type: PANEL_TYPES.TABLE, - label: intl.formatMessage({ - id: 'visTypeTimeseries.visPicker.tableLabel', - defaultMessage: 'Table', - }), - }, - ].map((item) => { - return ( - - ); - }); - - return {tabs}; -}); - -VisPicker.propTypes = { - model: PropTypes.object, - onChange: PropTypes.func, -}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_picker.tsx b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.tsx new file mode 100644 index 0000000000000..74ef710b1656f --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_picker.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 React from 'react'; +import { EuiTabs, EuiTab } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { PANEL_TYPES } from '../../../common/panel_types'; +import { TimeseriesVisParams } from '../../types'; + +const tabs = [ + { + type: PANEL_TYPES.TIMESERIES, + label: i18n.translate('visTypeTimeseries.visPicker.timeSeriesLabel', { + defaultMessage: 'Time Series', + }), + }, + { + type: PANEL_TYPES.METRIC, + label: i18n.translate('visTypeTimeseries.visPicker.metricLabel', { + defaultMessage: 'Metric', + }), + }, + { + type: PANEL_TYPES.TOP_N, + label: i18n.translate('visTypeTimeseries.visPicker.topNLabel', { + defaultMessage: 'Top N', + }), + }, + { + type: PANEL_TYPES.GAUGE, + label: i18n.translate('visTypeTimeseries.visPicker.gaugeLabel', { + defaultMessage: 'Gauge', + }), + }, + { type: PANEL_TYPES.MARKDOWN, label: 'Markdown' }, + { + type: PANEL_TYPES.TABLE, + label: i18n.translate('visTypeTimeseries.visPicker.tableLabel', { + defaultMessage: 'Table', + }), + }, +]; + +interface VisPickerProps { + onChange: (partialModel: Partial) => void; + currentVisType: TimeseriesVisParams['type']; +} + +export const VisPicker = ({ onChange, currentVisType }: VisPickerProps) => { + return ( + + {tabs.map(({ label, type }) => ( + onChange({ type })} + data-test-subj={`${type}TsvbTypeBtn`} + > + {label} + + ))} + + ); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts index 8b638e1f41131..150a3a716a879 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts @@ -11,7 +11,7 @@ import React, { lazy } from 'react'; import { IUiSettingsClient } from 'src/core/public'; import { PersistedState } from 'src/plugins/visualizations/public'; -import { TimeseriesVisParams } from '../../../metrics_fn'; +import { TimeseriesVisParams } from '../../../types'; import { TimeseriesVisData } from '../../../../common/types'; /** diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.js b/src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.js rename to src/plugins/vis_type_timeseries/public/application/contexts/form_validation_context.ts diff --git a/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js b/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.ts similarity index 68% rename from src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js rename to src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.ts index 1a953be530eb7..c03202b5fb4e2 100644 --- a/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.js +++ b/src/plugins/vis_type_timeseries/public/application/contexts/vis_data_context.ts @@ -7,5 +7,6 @@ */ import React from 'react'; +import { TimeseriesVisData } from 'src/plugins/vis_type_timeseries/common/types'; -export const VisDataContext = React.createContext({}); +export const VisDataContext = React.createContext({} as TimeseriesVisData); diff --git a/src/plugins/vis_type_timeseries/public/application/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.tsx similarity index 59% rename from src/plugins/vis_type_timeseries/public/application/editor_controller.js rename to src/plugins/vis_type_timeseries/public/application/editor_controller.tsx index f33382982abfd..e2ad9cf90bb30 100644 --- a/src/plugins/vis_type_timeseries/public/application/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.tsx @@ -8,37 +8,36 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { EventEmitter } from 'events'; + +import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { IEditorController, EditorRenderProps } from 'src/plugins/visualize/public'; import { getUISettings, getI18n } from '../services'; import { VisEditor } from './components/vis_editor_lazy'; +import { TimeseriesVisParams } from '../types'; export const TSVB_EDITOR_NAME = 'tsvbEditor'; -export class EditorController { - constructor(el, vis, eventEmitter, embeddableHandler) { - this.el = el; - - this.embeddableHandler = embeddableHandler; - this.eventEmitter = eventEmitter; - - this.state = { - vis: vis, - }; - } +export class EditorController implements IEditorController { + constructor( + private el: HTMLElement, + private vis: Vis, + private eventEmitter: EventEmitter, + private embeddableHandler: VisualizeEmbeddableContract + ) {} - async render(params) { + render({ timeRange, uiState }: EditorRenderProps) { const I18nContext = getI18n().Context; render( {}} - appState={params.appState} + vis={this.vis} + timeRange={timeRange} embeddableHandler={this.embeddableHandler} eventEmitter={this.eventEmitter} + uiState={uiState} /> , this.el diff --git a/src/plugins/vis_type_timeseries/public/application/index.ts b/src/plugins/vis_type_timeseries/public/application/index.ts index de34e91fd4798..fcc0c592b1ef5 100644 --- a/src/plugins/vis_type_timeseries/public/application/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/index.ts @@ -6,6 +6,5 @@ * Side Public License, v 1. */ -// @ts-ignore export { EditorController, TSVB_EDITOR_NAME } from './editor_controller'; export * from './lib'; diff --git a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts index 4b41747f23c79..088930f90a765 100644 --- a/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts +++ b/src/plugins/vis_type_timeseries/public/application/lib/fetch_fields.ts @@ -11,10 +11,12 @@ import { getCoreStart, getDataStart } from '../../services'; import { ROUTES } from '../../../common/constants'; import { SanitizedFieldType } from '../../../common/types'; +export type VisFields = Record; + export async function fetchFields( indexes: string[] = [], signal?: AbortSignal -): Promise> { +): Promise { const patterns = Array.isArray(indexes) ? indexes : [indexes]; const coreStart = getCoreStart(); const dataStart = getDataStart(); @@ -32,7 +34,7 @@ export async function fetchFields( }) ); - const fields: Record = patterns.reduce( + const fields: VisFields = patterns.reduce( (cumulatedFields, currentPattern, index) => ({ ...cumulatedFields, [currentPattern]: indexFields[index], diff --git a/src/plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts index 8ad42dea407e2..aec5013f4eb80 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_fn.ts @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { KibanaContext } from '../../data/public'; import { ExpressionFunctionDefinition, Render } from '../../expressions/public'; -import { PanelSchema, TimeseriesVisData } from '../common/types'; +import { TimeseriesVisData } from '../common/types'; import { metricsRequestHandler } from './request_handler'; +import { TimeseriesVisParams } from './types'; type Input = KibanaContext | null; type Output = Promise>; @@ -21,8 +22,6 @@ interface Arguments { uiState: string; } -export type TimeseriesVisParams = PanelSchema; - export interface TimeseriesRenderValue { visData: TimeseriesVisData | {}; visParams: TimeseriesVisParams; diff --git a/src/plugins/vis_type_timeseries/public/request_handler.ts b/src/plugins/vis_type_timeseries/public/request_handler.ts index d0526f7e1d886..bf3779674b6ea 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.ts +++ b/src/plugins/vis_type_timeseries/public/request_handler.ts @@ -11,7 +11,7 @@ import { KibanaContext } from '../../data/public'; import { getTimezone, validateInterval } from './application'; import { getUISettings, getDataStart, getCoreStart } from './services'; import { MAX_BUCKETS_SETTING, ROUTES } from '../common/constants'; -import { TimeseriesVisParams } from './metrics_fn'; +import { TimeseriesVisParams } from './types'; import { TimeseriesVisData } from '../common/types'; interface MetricsRequestHandlerParams { diff --git a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx index 1f992cb2db511..06c5d20f08a7c 100644 --- a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx @@ -13,8 +13,9 @@ import { IUiSettingsClient } from 'kibana/public'; import type { PersistedState } from '../../visualizations/public'; import { VisualizationContainer } from '../../visualizations/public'; import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; -import { TimeseriesRenderValue, TimeseriesVisParams } from './metrics_fn'; +import { TimeseriesRenderValue } from './metrics_fn'; import { TimeseriesVisData } from '../common/types'; +import { TimeseriesVisParams } from './types'; const TimeseriesVisualization = lazy( () => import('./application/components/timeseries_visualization') diff --git a/src/plugins/vis_type_timeseries/public/to_ast.ts b/src/plugins/vis_type_timeseries/public/to_ast.ts index ebceab333d422..90d57218da28c 100644 --- a/src/plugins/vis_type_timeseries/public/to_ast.ts +++ b/src/plugins/vis_type_timeseries/public/to_ast.ts @@ -8,7 +8,8 @@ import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { Vis } from '../../visualizations/public'; -import { TimeseriesExpressionFunctionDefinition, TimeseriesVisParams } from './metrics_fn'; +import { TimeseriesExpressionFunctionDefinition } from './metrics_fn'; +import { TimeseriesVisParams } from './types'; export const toExpressionAst = (vis: Vis) => { const timeseries = buildExpressionFunction('tsvb', { diff --git a/src/plugins/vis_type_timeseries/public/types.ts b/src/plugins/vis_type_timeseries/public/types.ts index 5c6dcfdcf6599..2986ba6d45f83 100644 --- a/src/plugins/vis_type_timeseries/public/types.ts +++ b/src/plugins/vis_type_timeseries/public/types.ts @@ -8,6 +8,7 @@ import React from 'react'; import { EuiDraggable } from '@elastic/eui'; +import { PanelSchema } from '../common/types'; type PropsOf = T extends React.ComponentType ? ComponentProps : never; type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: any[]) => any @@ -16,3 +17,5 @@ type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: a export type DragHandleProps = FirstArgumentOf< Exclude['children'], React.ReactElement> >['dragHandleProps']; + +export type TimeseriesVisParams = PanelSchema; diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 68c1bb3e0c87d..4dd14a6a4a5a1 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -39,12 +39,12 @@ export interface SerializedVisData { savedSearchId?: string; } -export interface SerializedVis { +export interface SerializedVis { id?: string; title: string; description?: string; type: string; - params: VisParams; + params: T; uiState?: any; data: SerializedVisData; } @@ -80,7 +80,7 @@ export class Vis { public readonly uiState: PersistedState; - constructor(visType: string, visState: SerializedVis = {} as any) { + constructor(visType: string, visState: SerializedVis = {} as any) { this.type = this.getType(visType); this.params = this.getParams(visState.params); this.uiState = new PersistedState(visState.uiState); @@ -154,9 +154,9 @@ export class Vis { } } - clone() { + clone(): Vis { const { data, ...restOfSerialized } = this.serialize(); - const vis = new Vis(this.type.name, restOfSerialized as any); + const vis = new Vis(this.type.name, restOfSerialized as any); vis.setState({ ...restOfSerialized, data: {} }); const aggs = this.data.indexPattern ? getAggs().createAggConfigs(this.data.indexPattern, data.aggs) @@ -175,7 +175,7 @@ export class Vis { title: this.title, description: this.description, type: this.type.name, - params: cloneDeep(this.params) as any, + params: cloneDeep(this.params), uiState: this.uiState.toJSON(), data: { aggs: aggs as any, diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 67c3d22d95426..da18b3b97a522 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -15,6 +15,7 @@ import { VisualizeEmbeddableContract, VisSavedObject, PersistedState, + VisParams, } from 'src/plugins/visualizations/public'; import { CoreStart, @@ -114,9 +115,9 @@ export interface ByValueVisInstance { export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; -export type VisEditorConstructor = new ( +export type VisEditorConstructor = new ( element: HTMLElement, - vis: Vis, + vis: Vis, eventEmitter: EventEmitter, embeddableHandler: VisualizeEmbeddableContract ) => IEditorController; diff --git a/src/plugins/visualize/public/vis_editors_registry.ts b/src/plugins/visualize/public/vis_editors_registry.ts index 42dd89c2d9042..2cb018e78954b 100644 --- a/src/plugins/visualize/public/vis_editors_registry.ts +++ b/src/plugins/visualize/public/vis_editors_registry.ts @@ -11,13 +11,13 @@ import { VisEditorConstructor } from './application/types'; const DEFAULT_NAME = 'default'; export const createVisEditorsRegistry = () => { - const map = new Map(); + const map = new Map>(); return { registerDefault: (editor: VisEditorConstructor) => { map.set(DEFAULT_NAME, editor); }, - register: (name: string, editor: VisEditorConstructor) => { + register: (name: string, editor: VisEditorConstructor) => { if (name) { map.set(name, editor); } From 1618e5436b23d9e4f631c678d65fbefaa54b20ad Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 11 Mar 2021 10:23:51 +0100 Subject: [PATCH 23/26] [Console] Update copy when showing warnings in response headers (#94270) * remove "deprecated: " from console warning * refactor "deprecation" to "warning" * complete name refactor in test files --- .../send_request_to_es.ts | 6 ++-- src/plugins/console/public/lib/utils/index.ts | 4 +-- .../console/public/lib/utils/utils.test.js | 32 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts index 898d8e809fca1..aeaa2f76816e4 100644 --- a/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts +++ b/src/plugins/console/public/application/hooks/use_send_current_request_to_es/send_request_to_es.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { extractDeprecationMessages } from '../../../lib/utils'; +import { extractWarningMessages } from '../../../lib/utils'; import { XJson } from '../../../../../es_ui_shared/public'; const { collapseLiteralStrings } = XJson; // @ts-ignore @@ -88,8 +88,8 @@ export function sendRequestToES(args: EsRequestArgs): Promise const warnings = xhr.getResponseHeader('warning'); if (warnings) { - const deprecationMessages = extractDeprecationMessages(warnings); - value = deprecationMessages.join('\n') + '\n' + value; + const warningMessages = extractWarningMessages(warnings); + value = warningMessages.join('\n') + '\n' + value; } if (isMultiRequest) { diff --git a/src/plugins/console/public/lib/utils/index.ts b/src/plugins/console/public/lib/utils/index.ts index 0aac2d01ad758..71b305807e61d 100644 --- a/src/plugins/console/public/lib/utils/index.ts +++ b/src/plugins/console/public/lib/utils/index.ts @@ -48,14 +48,14 @@ export function formatRequestBodyDoc(data: string[], indent: boolean) { }; } -export function extractDeprecationMessages(warnings: string) { +export function extractWarningMessages(warnings: string) { // pattern for valid warning header const re = /\d{3} [0-9a-zA-Z!#$%&'*+-.^_`|~]+ \"((?:\t| |!|[\x23-\x5b]|[\x5d-\x7e]|[\x80-\xff]|\\\\|\\")*)\"(?: \"[^"]*\")?/; // split on any comma that is followed by an even number of quotes return _.map(splitOnUnquotedCommaSpace(warnings), (warning) => { const match = re.exec(warning); // extract the actual warning if there was a match - return '#! Deprecation: ' + (match !== null ? unescape(match[1]) : warning); + return '#! ' + (match !== null ? unescape(match[1]) : warning); }); } diff --git a/src/plugins/console/public/lib/utils/utils.test.js b/src/plugins/console/public/lib/utils/utils.test.js index ff851bbea3c46..d7fc690e1bc24 100644 --- a/src/plugins/console/public/lib/utils/utils.test.js +++ b/src/plugins/console/public/lib/utils/utils.test.js @@ -11,51 +11,51 @@ import * as utils from '.'; describe('Utils class', () => { test('extract deprecation messages', function () { expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning" "Mon, 27 Feb 2017 14:52:14 GMT"' ) - ).toEqual(['#! Deprecation: this is a warning']); + ).toEqual(['#! this is a warning']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning"' ) - ).toEqual(['#! Deprecation: this is a warning']); + ).toEqual(['#! this is a warning']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning" "Mon, 27 Feb 2017 14:52:14 GMT", 299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a second warning" "Mon, 27 Feb 2017 14:52:14 GMT"' ) - ).toEqual(['#! Deprecation: this is a warning', '#! Deprecation: this is a second warning']); + ).toEqual(['#! this is a warning', '#! this is a second warning']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning", 299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a second warning"' ) - ).toEqual(['#! Deprecation: this is a warning', '#! Deprecation: this is a second warning']); + ).toEqual(['#! this is a warning', '#! this is a second warning']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes a comma" "Mon, 27 Feb 2017 14:52:14 GMT"' ) - ).toEqual(['#! Deprecation: this is a warning, and it includes a comma']); + ).toEqual(['#! this is a warning, and it includes a comma']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes a comma"' ) - ).toEqual(['#! Deprecation: this is a warning, and it includes a comma']); + ).toEqual(['#! this is a warning, and it includes a comma']); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes an escaped backslash \\\\ and a pair of \\"escaped quotes\\"" "Mon, 27 Feb 2017 14:52:14 GMT"' ) ).toEqual([ - '#! Deprecation: this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', + '#! this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', ]); expect( - utils.extractDeprecationMessages( + utils.extractWarningMessages( '299 Elasticsearch-6.0.0-alpha1-SNAPSHOT-abcdef1 "this is a warning, and it includes an escaped backslash \\\\ and a pair of \\"escaped quotes\\""' ) ).toEqual([ - '#! Deprecation: this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', + '#! this is a warning, and it includes an escaped backslash \\ and a pair of "escaped quotes"', ]); }); From 716e2f78166346267bae9ae2190c2d35cad8ec01 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Thu, 11 Mar 2021 12:11:24 +0000 Subject: [PATCH 24/26] [Task Manager][Docs] fixes the formatting of an "Important" box (#94276) Fixes the rendering of the Important callout on: Task Manager Production Considerations --- .../task-manager-production-considerations.asciidoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/user/production-considerations/task-manager-production-considerations.asciidoc b/docs/user/production-considerations/task-manager-production-considerations.asciidoc index 39835919a7fd4..606f113b2274f 100644 --- a/docs/user/production-considerations/task-manager-production-considerations.asciidoc +++ b/docs/user/production-considerations/task-manager-production-considerations.asciidoc @@ -12,11 +12,11 @@ This has three major benefits: [IMPORTANT] ============================================== - Task definitions for alerts and actions are stored in the index specified by <>. - The default is `.kibana_task_manager`. +Task definitions for alerts and actions are stored in the index specified by <>. The default is `.kibana_task_manager`. - You must have at least one replica of this index for production deployments. - If you lose this index, all scheduled alerts and actions are lost. +You must have at least one replica of this index for production deployments. + +If you lose this index, all scheduled alerts and actions are lost. ============================================== [float] From 33fbe74e4ed2e24d9ddb12d386f3860c371d57f9 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 11 Mar 2021 13:51:50 +0100 Subject: [PATCH 25/26] [Lens] Transpose columns (#89748) --- api_docs/lens.json | 8 +- .../__snapshots__/table_basic.test.tsx.snap | 6 +- .../components/columns.tsx | 99 ++-- .../components/dimension_editor.tsx | 71 +-- .../components/table_actions.test.ts | 147 ++++++ .../components/table_actions.ts | 48 +- .../components/table_basic.tsx | 11 + .../expression.test.tsx | 2 +- .../datatable_visualization/expression.tsx | 25 +- .../transpose_helpers.test.ts | 293 +++++++++++ .../transpose_helpers.ts | 237 +++++++++ .../visualization.test.tsx | 74 +-- .../datatable_visualization/visualization.tsx | 86 ++- .../draggable_dimension_button.tsx | 3 + .../config_panel/empty_dimension_button.tsx | 4 + .../editor_frame/config_panel/layer_panel.tsx | 38 +- .../dimension_panel/dimension_editor.tsx | 12 + .../dimension_panel/dimension_panel.test.tsx | 1 + .../dimension_panel/droppable.test.ts | 492 +++++++++++++++++- .../dimension_panel/droppable.ts | 49 +- .../dimension_panel/reference_editor.test.tsx | 1 + .../dimension_panel/reference_editor.tsx | 20 +- .../indexpattern_datasource/indexpattern.tsx | 6 +- .../indexpattern_suggestions.ts | 11 + .../operations/layer_helpers.test.ts | 30 ++ .../operations/layer_helpers.ts | 95 +++- x-pack/plugins/lens/public/types.ts | 12 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../functional/apps/lens/drag_and_drop.ts | 16 +- .../test/functional/apps/lens/smokescreen.ts | 4 +- x-pack/test/functional/apps/lens/table.ts | 27 +- 32 files changed, 1748 insertions(+), 182 deletions(-) create mode 100644 x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.test.ts create mode 100644 x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts diff --git a/api_docs/lens.json b/api_docs/lens.json index 7e30ec6a15c3e..1c7581a8a1db6 100644 --- a/api_docs/lens.json +++ b/api_docs/lens.json @@ -107,7 +107,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/datatable_visualization/visualization.tsx", - "lineNumber": 35 + "lineNumber": 43 }, "signature": [ { @@ -128,7 +128,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/datatable_visualization/visualization.tsx", - "lineNumber": 36 + "lineNumber": 44 } }, { @@ -139,7 +139,7 @@ "description": [], "source": { "path": "x-pack/plugins/lens/public/datatable_visualization/visualization.tsx", - "lineNumber": 37 + "lineNumber": 45 }, "signature": [ { @@ -155,7 +155,7 @@ ], "source": { "path": "x-pack/plugins/lens/public/datatable_visualization/visualization.tsx", - "lineNumber": 34 + "lineNumber": 42 }, "initialIsOpen": false }, diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap index 992301af13ad0..afc69c2e8861f 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap +++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap @@ -537,7 +537,7 @@ exports[`DatatableComponent it should not render actions on header when it is in Array [ Object { "actions": Object { - "additional": undefined, + "additional": Array [], "showHide": false, "showMoveLeft": false, "showMoveRight": false, @@ -551,7 +551,7 @@ exports[`DatatableComponent it should not render actions on header when it is in }, Object { "actions": Object { - "additional": undefined, + "additional": Array [], "showHide": false, "showMoveLeft": false, "showMoveRight": false, @@ -565,7 +565,7 @@ exports[`DatatableComponent it should not render actions on header when it is in }, Object { "actions": Object { - "additional": undefined, + "additional": Array [], "showHide": false, "showMoveLeft": false, "showMoveRight": false, diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx index fdb05599c38e9..ba24da8309ed7 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/columns.tsx @@ -7,8 +7,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiDataGridColumn, EuiDataGridColumnCellActionProps } from '@elastic/eui'; -import type { Datatable, DatatableColumnMeta } from 'src/plugins/expressions'; +import { + EuiDataGridColumn, + EuiDataGridColumnCellActionProps, + EuiListGroupItemProps, +} from '@elastic/eui'; +import type { Datatable, DatatableColumn, DatatableColumnMeta } from 'src/plugins/expressions'; import type { FormatFactory } from '../../types'; import { ColumnConfig } from './table_basic'; @@ -22,6 +26,10 @@ export const createGridColumns = ( rowIndex: number, negate?: boolean ) => void, + handleTransposedColumnClick: ( + bucketValues: Array<{ originalBucketColumn: DatatableColumn; value: unknown }>, + negate?: boolean + ) => void, isReadOnly: boolean, columnConfig: ColumnConfig, visibleColumns: string[], @@ -135,9 +143,63 @@ export const createGridColumns = ( ] : undefined; - const column = columnConfig.columns.find(({ columnId }) => columnId === field); - const initialWidth = column?.width; - const isHidden = column?.hidden; + const columnArgs = columnConfig.columns.find(({ columnId }) => columnId === field); + const isTransposed = Boolean(columnArgs?.originalColumnId); + const initialWidth = columnArgs?.width; + const isHidden = columnArgs?.hidden; + const originalColumnId = columnArgs?.originalColumnId; + + const additionalActions: EuiListGroupItemProps[] = []; + + if (!isReadOnly) { + additionalActions.push({ + color: 'text', + size: 'xs', + onClick: () => onColumnResize({ columnId: originalColumnId || field, width: undefined }), + iconType: 'empty', + label: i18n.translate('xpack.lens.table.resize.reset', { + defaultMessage: 'Reset width', + }), + 'data-test-subj': 'lensDatatableResetWidth', + isDisabled: initialWidth == null, + }); + if (!isTransposed) { + additionalActions.push({ + color: 'text', + size: 'xs', + onClick: () => onColumnHide({ columnId: originalColumnId || field }), + iconType: 'eyeClosed', + label: i18n.translate('xpack.lens.table.hide.hideLabel', { + defaultMessage: 'Hide', + }), + 'data-test-subj': 'lensDatatableHide', + isDisabled: !isHidden && visibleColumns.length <= 1, + }); + } else if (columnArgs?.bucketValues) { + const bucketValues = columnArgs?.bucketValues; + additionalActions.push({ + color: 'text', + size: 'xs', + onClick: () => handleTransposedColumnClick(bucketValues, false), + iconType: 'plusInCircle', + label: i18n.translate('xpack.lens.table.columnFilter.filterForValueText', { + defaultMessage: 'Filter for column', + }), + 'data-test-subj': 'lensDatatableHide', + }); + + additionalActions.push({ + color: 'text', + size: 'xs', + onClick: () => handleTransposedColumnClick(bucketValues, true), + iconType: 'minusInCircle', + label: i18n.translate('xpack.lens.table.columnFilter.filterOutValueText', { + defaultMessage: 'Filter out column', + }), + 'data-test-subj': 'lensDatatableHide', + }); + } + } const columnDefinition: EuiDataGridColumn = { id: field, @@ -162,32 +224,7 @@ export const createGridColumns = ( defaultMessage: 'Sort descending', }), }, - additional: isReadOnly - ? undefined - : [ - { - color: 'text', - size: 'xs', - onClick: () => onColumnResize({ columnId: field, width: undefined }), - iconType: 'empty', - label: i18n.translate('xpack.lens.table.resize.reset', { - defaultMessage: 'Reset width', - }), - 'data-test-subj': 'lensDatatableResetWidth', - isDisabled: initialWidth == null, - }, - { - color: 'text', - size: 'xs', - onClick: () => onColumnHide({ columnId: field }), - iconType: 'eyeClosed', - label: i18n.translate('xpack.lens.table.hide.hideLabel', { - defaultMessage: 'Hide', - }), - 'data-test-subj': 'lensDatatableHide', - isDisabled: !isHidden && visibleColumns.length <= 1, - }, - ], + additional: additionalActions, }, }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx index 9c60cd47af3e3..672b29846d760 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSwitch, EuiButtonGroup, htmlIdGenerator } from '@elastic/eui'; import { VisualizationDimensionEditorProps } from '../../types'; import { DatatableVisualizationState } from '../visualization'; +import { getOriginalId } from '../transpose_helpers'; const idPrefix = htmlIdGenerator()(); @@ -20,13 +21,15 @@ export function TableDimensionEditor( const column = state.columns.find(({ columnId }) => accessor === columnId); if (!column) return null; + if (column.isTransposed) return null; // either read config state or use same logic as chart itself const currentAlignment = column?.alignment || (frame.activeData && - frame.activeData[state.layerId].columns.find((col) => col.id === accessor)?.meta.type === - 'number' + frame.activeData[state.layerId].columns.find( + (col) => col.id === accessor || getOriginalId(col.id) === accessor + )?.meta.type === 'number' ? 'right' : 'left'); @@ -89,39 +92,41 @@ export function TableDimensionEditor( }} /> - + display="columnCompressedSwitch" + > + { + const newState = { + ...state, + columns: state.columns.map((currentColumn) => { + if (currentColumn.columnId === accessor) { + return { + ...currentColumn, + hidden: !column.hidden, + }; + } else { + return currentColumn; + } + }), + }; + setState(newState); + }} + /> + + )} ); } diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts index 68416ac9a60aa..8490d33f83444 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.test.ts @@ -15,9 +15,11 @@ import { createGridResizeHandler, createGridSortingConfig, createGridHideHandler, + createTransposeColumnFilterHandler, } from './table_actions'; import { LensGridDirection } from './types'; import { ColumnConfig } from './table_basic'; +import { LensMultiTable } from '../../types'; function getDefaultConfig(): ColumnConfig { return { @@ -48,6 +50,19 @@ function createTableRef( }; } +function createUntransposedRef(options?: { + withDate: boolean; +}): React.MutableRefObject { + return { + current: { + type: 'lens_multitable', + tables: { + first: createTableRef(options).current, + }, + }, + }; +} + describe('Table actions', () => { const onEditAction = jest.fn(); @@ -132,6 +147,138 @@ describe('Table actions', () => { }); }); }); + + describe('Transposed column filtering', () => { + it('should set a filter on click with the correct configuration', () => { + const onClickValue = jest.fn(); + const tableRef = createUntransposedRef({ withDate: true }); + tableRef.current.tables.first.rows = [{ a: 123456 }]; + const filterHandle = createTransposeColumnFilterHandler(onClickValue, tableRef); + + filterHandle( + [ + { + originalBucketColumn: tableRef.current.tables.first.columns[0], + value: 123456, + }, + ], + false + ); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: tableRef.current.tables.first, + value: 123456, + }, + ], + negate: false, + timeFieldName: 'a', + }); + }); + + it('should set a negate filter on click with the correct configuration', () => { + const onClickValue = jest.fn(); + const tableRef = createUntransposedRef({ withDate: true }); + tableRef.current.tables.first.rows = [{ a: 123456 }]; + const filterHandle = createTransposeColumnFilterHandler(onClickValue, tableRef); + + filterHandle( + [ + { + originalBucketColumn: tableRef.current.tables.first.columns[0], + value: 123456, + }, + ], + true + ); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: tableRef.current.tables.first, + value: 123456, + }, + ], + negate: true, + timeFieldName: undefined, + }); + }); + + it('should set a multi filter and look up positions of the values', () => { + const onClickValue = jest.fn(); + const tableRef = createUntransposedRef({ withDate: false }); + const filterHandle = createTransposeColumnFilterHandler(onClickValue, tableRef); + tableRef.current.tables.first.columns = [ + { + id: 'a', + name: 'a', + meta: { + type: 'string', + }, + }, + { + id: 'b', + name: 'b', + meta: { + type: 'string', + }, + }, + ]; + tableRef.current.tables.first.rows = [ + { + a: 'a1', + b: 'b1', + }, + { + a: 'a2', + b: 'b2', + }, + { + a: 'a3', + b: 'b3', + }, + { + a: 'a4', + b: 'b4', + }, + ]; + + filterHandle( + [ + { + originalBucketColumn: tableRef.current.tables.first.columns[0], + value: 'a2', + }, + { + originalBucketColumn: tableRef.current.tables.first.columns[1], + value: 'b3', + }, + ], + false + ); + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 1, + table: tableRef.current.tables.first, + value: 'a2', + }, + { + column: 1, + row: 2, + table: tableRef.current.tables.first, + value: 'b3', + }, + ], + negate: false, + timeFieldName: undefined, + }); + }); + }); describe('Table sorting', () => { it('should create the right configuration for all types of sorting', () => { const configs: Array<{ diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts index 4f0271b758ffb..0d44ae3aa6dec 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_actions.ts @@ -6,8 +6,8 @@ */ import type { EuiDataGridSorting } from '@elastic/eui'; -import type { Datatable } from 'src/plugins/expressions'; -import type { LensFilterEvent } from '../../types'; +import type { Datatable, DatatableColumn } from 'src/plugins/expressions'; +import type { LensFilterEvent, LensMultiTable } from '../../types'; import type { LensGridDirection, LensResizeAction, @@ -17,18 +17,20 @@ import type { import { ColumnConfig } from './table_basic'; import { desanitizeFilterContext } from '../../utils'; +import { getOriginalId } from '../transpose_helpers'; export const createGridResizeHandler = ( columnConfig: ColumnConfig, setColumnConfig: React.Dispatch>, onEditAction: (data: LensResizeAction['data']) => void ) => (eventData: { columnId: string; width: number | undefined }) => { + const originalColumnId = getOriginalId(eventData.columnId); // directly set the local state of the component to make sure the visualization re-renders immediately, // re-layouting and taking up all of the available space. setColumnConfig({ ...columnConfig, columns: columnConfig.columns.map((column) => { - if (column.columnId === eventData.columnId) { + if (column.columnId === eventData.columnId || column.originalColumnId === originalColumnId) { return { ...column, width: eventData.width }; } return column; @@ -36,7 +38,7 @@ export const createGridResizeHandler = ( }); return onEditAction({ action: 'resize', - columnId: eventData.columnId, + columnId: originalColumnId, width: eventData.width, }); }; @@ -46,11 +48,12 @@ export const createGridHideHandler = ( setColumnConfig: React.Dispatch>, onEditAction: (data: LensToggleAction['data']) => void ) => (eventData: { columnId: string }) => { + const originalColumnId = getOriginalId(eventData.columnId); // directly set the local state of the component to make sure the visualization re-renders immediately setColumnConfig({ ...columnConfig, columns: columnConfig.columns.map((column) => { - if (column.columnId === eventData.columnId) { + if (column.columnId === eventData.columnId || column.originalColumnId === originalColumnId) { return { ...column, hidden: true }; } return column; @@ -58,7 +61,7 @@ export const createGridHideHandler = ( }); return onEditAction({ action: 'toggle', - columnId: eventData.columnId, + columnId: originalColumnId, }); }; @@ -92,6 +95,39 @@ export const createGridFilterHandler = ( onClickValue(desanitizeFilterContext(data)); }; +export const createTransposeColumnFilterHandler = ( + onClickValue: (data: LensFilterEvent['data']) => void, + untransposedDataRef: React.MutableRefObject +) => ( + bucketValues: Array<{ originalBucketColumn: DatatableColumn; value: unknown }>, + negate: boolean = false +) => { + if (!untransposedDataRef.current) return; + const originalTable = Object.values(untransposedDataRef.current.tables)[0]; + const timeField = bucketValues.find( + ({ originalBucketColumn }) => originalBucketColumn.meta.type === 'date' + )?.originalBucketColumn; + const isDate = Boolean(timeField); + const timeFieldName = negate && isDate ? undefined : timeField?.meta?.field; + + const data: LensFilterEvent['data'] = { + negate, + data: bucketValues.map(({ originalBucketColumn, value }) => { + const columnIndex = originalTable.columns.findIndex((c) => c.id === originalBucketColumn.id); + const rowIndex = originalTable.rows.findIndex((r) => r[originalBucketColumn.id] === value); + return { + row: rowIndex, + column: columnIndex, + value, + table: originalTable, + }; + }), + timeFieldName, + }; + + onClickValue(desanitizeFilterContext(data)); +}; + export const createGridSortingConfig = ( sortBy: string | undefined, sortDirection: LensGridDirection, diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx index e1687ba28f07b..24cde07cebaa0 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx @@ -38,6 +38,7 @@ import { createGridHideHandler, createGridResizeHandler, createGridSortingConfig, + createTransposeColumnFilterHandler, } from './table_actions'; export const DataContext = React.createContext({}); @@ -82,6 +83,9 @@ export const DatatableComponent = (props: DatatableRenderProps) => { const firstTableRef = useRef(firstLocalTable); firstTableRef.current = firstLocalTable; + const untransposedDataRef = useRef(props.untransposedData); + untransposedDataRef.current = props.untransposedData; + const hasAtLeastOneRowClickAction = props.rowHasRowClickTriggerActions?.some((x) => x); const { getType, dispatchEvent, renderMode, formatFactory } = props; @@ -125,6 +129,11 @@ export const DatatableComponent = (props: DatatableRenderProps) => { onClickValue, ]); + const handleTransposedColumnClick = useMemo( + () => createTransposeColumnFilterHandler(onClickValue, untransposedDataRef), + [onClickValue, untransposedDataRef] + ); + const bucketColumns = useMemo( () => columnConfig.columns @@ -172,6 +181,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { bucketColumns, firstLocalTable, handleFilterClick, + handleTransposedColumnClick, isReadOnlySorted, columnConfig, visibleColumns, @@ -183,6 +193,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { bucketColumns, firstLocalTable, handleFilterClick, + handleTransposedColumnClick, isReadOnlySorted, columnConfig, visibleColumns, diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index 3ee41d4e9aeed..3ba448b49afc9 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -73,7 +73,7 @@ function sampleArgs() { type: 'lens_datatable_column', }, ], - sortingColumnId: '', + sortingColumnId: undefined, sortingDirection: 'none', }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index f6a38541cda27..7d879217abf8b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -7,11 +7,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import type { IAggType } from 'src/plugins/data/public'; -import type { +import { DatatableColumnMeta, ExpressionFunctionDefinition, ExpressionRenderDefinition, @@ -23,8 +24,9 @@ import { ColumnState } from './visualization'; import type { FormatFactory, ILensInterpreterRenderHandlers, LensMultiTable } from '../types'; import type { DatatableRender } from './components/types'; +import { transposeTable } from './transpose_helpers'; -interface Args { +export interface Args { title: string; description?: string; columns: Array; @@ -34,6 +36,7 @@ interface Args { export interface DatatableProps { data: LensMultiTable; + untransposedData?: LensMultiTable; args: Args; } @@ -78,6 +81,7 @@ export const getDatatable = ({ }, }, fn(data, args, context) { + let untransposedData: LensMultiTable | undefined; // do the sorting at this level to propagate it also at CSV download const [firstTable] = Object.values(data.tables); const [layerId] = Object.keys(context.inspectorAdapters.tables || {}); @@ -86,6 +90,15 @@ export const getDatatable = ({ firstTable.columns.forEach((column) => { formatters[column.id] = formatFactory(column.meta?.params); }); + + const hasTransposedColumns = args.columns.some((c) => c.isTransposed); + if (hasTransposedColumns) { + // store original shape of data separately + untransposedData = cloneDeep(data); + // transposes table and args inplace + transposeTable(args, firstTable, formatters); + } + const { sortingColumnId: sortBy, sortingDirection: sortDirection } = args; const columnsReverseLookup = firstTable.columns.reduce< @@ -95,7 +108,7 @@ export const getDatatable = ({ return memo; }, {}); - if (sortBy && sortDirection !== 'none') { + if (sortBy && columnsReverseLookup[sortBy] && sortDirection !== 'none') { // Sort on raw values for these types, while use the formatted value for the rest const sortingCriteria = getSortingCriteria( isRange(columnsReverseLookup[sortBy]?.meta) @@ -111,12 +124,16 @@ export const getDatatable = ({ .sort(sortingCriteria); // replace also the local copy firstTable.rows = context.inspectorAdapters.tables[layerId].rows; + } else { + args.sortingColumnId = undefined; + args.sortingDirection = 'none'; } return { type: 'render', as: 'lens_datatable_renderer', value: { data, + untransposedData, args, }, }; @@ -141,6 +158,8 @@ export const datatableColumn: ExpressionFunctionDefinition< alignment: { types: ['string'], help: '' }, hidden: { types: ['boolean'], help: '' }, width: { types: ['number'], help: '' }, + isTransposed: { types: ['boolean'], help: '' }, + transposable: { types: ['boolean'], help: '' }, }, fn: function fn(input: unknown, args: ColumnState) { return { diff --git a/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.test.ts b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.test.ts new file mode 100644 index 0000000000000..91559a1778f4f --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.test.ts @@ -0,0 +1,293 @@ +/* + * 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 type { FieldFormat } from 'src/plugins/data/public'; +import type { Datatable } from 'src/plugins/expressions'; + +import { Args } from './expression'; +import { transposeTable } from './transpose_helpers'; + +describe('transpose_helpes', () => { + function buildTable(): Datatable { + // 3 buckets, 2 metrics + // first bucket goes A/B/C + // second buckets goes D/E/F + // third bucket goes X/Y/Z (all combinations) + // metric values count up from 1 + return { + type: 'datatable', + columns: [ + { id: 'bucket1', name: 'bucket1', meta: { type: 'string' } }, + { id: 'bucket2', name: 'bucket2', meta: { type: 'string' } }, + { id: 'bucket3', name: 'bucket3', meta: { type: 'string' } }, + { id: 'metric1', name: 'metric1', meta: { type: 'number' } }, + { id: 'metric2', name: 'metric2', meta: { type: 'number' } }, + ], + rows: [ + { bucket1: 'A', bucket2: 'D', bucket3: 'X', metric1: 1, metric2: 2 }, + { bucket1: 'A', bucket2: 'D', bucket3: 'Y', metric1: 3, metric2: 4 }, + { bucket1: 'A', bucket2: 'D', bucket3: 'Z', metric1: 5, metric2: 6 }, + { bucket1: 'A', bucket2: 'E', bucket3: 'X', metric1: 7, metric2: 8 }, + { bucket1: 'A', bucket2: 'E', bucket3: 'Y', metric1: 9, metric2: 10 }, + { bucket1: 'A', bucket2: 'E', bucket3: 'Z', metric1: 11, metric2: 12 }, + { bucket1: 'A', bucket2: 'F', bucket3: 'X', metric1: 13, metric2: 14 }, + { bucket1: 'A', bucket2: 'F', bucket3: 'Y', metric1: 15, metric2: 16 }, + { bucket1: 'A', bucket2: 'F', bucket3: 'Z', metric1: 17, metric2: 18 }, + { bucket1: 'B', bucket2: 'D', bucket3: 'X', metric1: 19, metric2: 20 }, + { bucket1: 'B', bucket2: 'D', bucket3: 'Y', metric1: 21, metric2: 22 }, + { bucket1: 'B', bucket2: 'D', bucket3: 'Z', metric1: 23, metric2: 24 }, + { bucket1: 'B', bucket2: 'E', bucket3: 'X', metric1: 25, metric2: 26 }, + { bucket1: 'B', bucket2: 'E', bucket3: 'Y', metric1: 27, metric2: 28 }, + { bucket1: 'B', bucket2: 'E', bucket3: 'Z', metric1: 29, metric2: 30 }, + { bucket1: 'B', bucket2: 'F', bucket3: 'X', metric1: 31, metric2: 32 }, + { bucket1: 'B', bucket2: 'F', bucket3: 'Y', metric1: 33, metric2: 34 }, + { bucket1: 'B', bucket2: 'F', bucket3: 'Z', metric1: 35, metric2: 36 }, + { bucket1: 'C', bucket2: 'D', bucket3: 'X', metric1: 37, metric2: 38 }, + { bucket1: 'C', bucket2: 'D', bucket3: 'Y', metric1: 39, metric2: 40 }, + { bucket1: 'C', bucket2: 'D', bucket3: 'Z', metric1: 41, metric2: 42 }, + { bucket1: 'C', bucket2: 'E', bucket3: 'X', metric1: 43, metric2: 44 }, + { bucket1: 'C', bucket2: 'E', bucket3: 'Y', metric1: 45, metric2: 46 }, + { bucket1: 'C', bucket2: 'E', bucket3: 'Z', metric1: 47, metric2: 48 }, + { bucket1: 'C', bucket2: 'F', bucket3: 'X', metric1: 49, metric2: 50 }, + { bucket1: 'C', bucket2: 'F', bucket3: 'Y', metric1: 51, metric2: 52 }, + { bucket1: 'C', bucket2: 'F', bucket3: 'Z', metric1: 53, metric2: 54 }, + ], + }; + } + + function buildArgs(): Args { + return { + title: 'Table', + sortingColumnId: undefined, + sortingDirection: 'none', + columns: [ + { + type: 'lens_datatable_column', + columnId: 'bucket1', + isTransposed: false, + transposable: false, + }, + { + type: 'lens_datatable_column', + columnId: 'bucket2', + isTransposed: false, + transposable: false, + }, + { + type: 'lens_datatable_column', + columnId: 'bucket3', + isTransposed: false, + transposable: false, + }, + { + type: 'lens_datatable_column', + columnId: 'metric1', + isTransposed: false, + transposable: true, + }, + { + type: 'lens_datatable_column', + columnId: 'metric2', + isTransposed: false, + transposable: true, + }, + ], + }; + } + + function buildFormatters() { + return ({ + bucket1: { convert: (x: unknown) => x }, + bucket2: { convert: (x: unknown) => x }, + bucket3: { convert: (x: unknown) => x }, + metric1: { convert: (x: unknown) => x }, + metric2: { convert: (x: unknown) => x }, + } as unknown) as Record; + } + + it('should transpose table by one column', () => { + const table = buildTable(); + const args = buildArgs(); + args.columns[0].isTransposed = true; + transposeTable(args, table, buildFormatters()); + + // one metric for each unique value of bucket1 + expect(table.columns.map((c) => c.id)).toEqual([ + 'bucket2', + 'bucket3', + 'A---metric1', + 'B---metric1', + 'C---metric1', + 'A---metric2', + 'B---metric2', + 'C---metric2', + ]); + + // order is different for args to visually group unique values + const expectedColumns = [ + 'bucket2', + 'bucket3', + 'A---metric1', + 'A---metric2', + 'B---metric1', + 'B---metric2', + 'C---metric1', + 'C---metric2', + ]; + + // args should be in sync + expect(args.columns.map((c) => c.columnId)).toEqual(expectedColumns); + // original column id should stay preserved + expect(args.columns.slice(2).map((c) => c.originalColumnId)).toEqual([ + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + ]); + + // data should stay consistent + expect(table.rows.length).toEqual(9); + table.rows.forEach((row, index) => { + expect(row['A---metric1']).toEqual(index * 2 + 1); + expect(row['A---metric2']).toEqual(index * 2 + 2); + // B metrics start with offset 18 because there are 18 A metrics (2 metrics * 3 bucket2 metrics * 3 bucket3 metrics) + expect(row['B---metric1']).toEqual(18 + index * 2 + 1); + expect(row['B---metric2']).toEqual(18 + index * 2 + 2); + // B metrics start with offset 36 because there are 18 A metrics and 18 B metrics (2 metrics * 3 bucket2 values * 3 bucket3 values) + expect(row['C---metric1']).toEqual(36 + index * 2 + 1); + expect(row['C---metric2']).toEqual(36 + index * 2 + 2); + }); + + // visible name should use separator + expect(table.columns[2].name).toEqual(`A › metric1`); + }); + + it('should transpose table by two columns', () => { + const table = buildTable(); + const args = buildArgs(); + args.columns[0].isTransposed = true; + args.columns[1].isTransposed = true; + transposeTable(args, table, buildFormatters()); + + // one metric for each unique value of bucket1 + expect(table.columns.map((c) => c.id)).toEqual([ + 'bucket3', + 'A---D---metric1', + 'B---D---metric1', + 'C---D---metric1', + 'A---E---metric1', + 'B---E---metric1', + 'C---E---metric1', + 'A---F---metric1', + 'B---F---metric1', + 'C---F---metric1', + 'A---D---metric2', + 'B---D---metric2', + 'C---D---metric2', + 'A---E---metric2', + 'B---E---metric2', + 'C---E---metric2', + 'A---F---metric2', + 'B---F---metric2', + 'C---F---metric2', + ]); + + // order is different for args to visually group unique values + const expectedColumns = [ + 'bucket3', + 'A---D---metric1', + 'A---D---metric2', + 'A---E---metric1', + 'A---E---metric2', + 'A---F---metric1', + 'A---F---metric2', + 'B---D---metric1', + 'B---D---metric2', + 'B---E---metric1', + 'B---E---metric2', + 'B---F---metric1', + 'B---F---metric2', + 'C---D---metric1', + 'C---D---metric2', + 'C---E---metric1', + 'C---E---metric2', + 'C---F---metric1', + 'C---F---metric2', + ]; + + // args should be in sync + expect(args.columns.map((c) => c.columnId)).toEqual(expectedColumns); + // original column id should stay preserved + expect(args.columns.slice(1).map((c) => c.originalColumnId)).toEqual([ + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + 'metric1', + 'metric2', + ]); + + // data should stay consistent + expect(table.rows.length).toEqual(3); + table.rows.forEach((row, index) => { + // each metric block has an additional offset of 6 because there are 6 metrics for each bucket1/bucket2 combination (2 metrics * 3 bucket3 values) + expect(row['A---D---metric1']).toEqual(index * 2 + 1); + expect(row['A---D---metric2']).toEqual(index * 2 + 2); + expect(row['A---E---metric1']).toEqual(index * 2 + 6 + 1); + expect(row['A---E---metric2']).toEqual(index * 2 + 6 + 2); + expect(row['A---F---metric1']).toEqual(index * 2 + 12 + 1); + expect(row['A---F---metric2']).toEqual(index * 2 + 12 + 2); + + expect(row['B---D---metric1']).toEqual(index * 2 + 18 + 1); + expect(row['B---D---metric2']).toEqual(index * 2 + 18 + 2); + expect(row['B---E---metric1']).toEqual(index * 2 + 24 + 1); + expect(row['B---E---metric2']).toEqual(index * 2 + 24 + 2); + expect(row['B---F---metric1']).toEqual(index * 2 + 30 + 1); + expect(row['B---F---metric2']).toEqual(index * 2 + 30 + 2); + + expect(row['C---D---metric1']).toEqual(index * 2 + 36 + 1); + expect(row['C---D---metric2']).toEqual(index * 2 + 36 + 2); + expect(row['C---E---metric1']).toEqual(index * 2 + 42 + 1); + expect(row['C---E---metric2']).toEqual(index * 2 + 42 + 2); + expect(row['C---F---metric1']).toEqual(index * 2 + 48 + 1); + expect(row['C---F---metric2']).toEqual(index * 2 + 48 + 2); + }); + }); + + it('should be able to handle missing values', () => { + const table = buildTable(); + const args = buildArgs(); + args.columns[0].isTransposed = true; + args.columns[1].isTransposed = true; + // delete A-E-Z bucket + table.rows.splice(5, 1); + transposeTable(args, table, buildFormatters()); + expect(args.columns.length).toEqual(19); + expect(table.columns.length).toEqual(19); + expect(table.rows.length).toEqual(3); + expect(table.rows[2]['A---E---metric1']).toEqual(undefined); + expect(table.rows[2]['A---E---metric2']).toEqual(undefined); + // 1 bucket column and 2 missing from the regular 18 columns + expect(Object.values(table.rows[2]).filter((val) => val !== undefined).length).toEqual( + 1 + 18 - 2 + ); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts new file mode 100644 index 0000000000000..6e29e018b481e --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/transpose_helpers.ts @@ -0,0 +1,237 @@ +/* + * 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 type { FieldFormat } from 'src/plugins/data/public'; +import type { Datatable, DatatableColumn, DatatableRow } from 'src/plugins/expressions'; + +import { Args } from './expression'; +import { ColumnState } from './visualization'; + +const TRANSPOSE_SEPARATOR = '---'; + +const TRANSPOSE_VISUAL_SEPARATOR = '›'; + +export function getTransposeId(value: string, columnId: string) { + return `${value}${TRANSPOSE_SEPARATOR}${columnId}`; +} + +export function getOriginalId(id: string) { + if (id.includes(TRANSPOSE_SEPARATOR)) { + const idParts = id.split(TRANSPOSE_SEPARATOR); + return idParts[idParts.length - 1]; + } + return id; +} + +/** + * Transposes the columns of the given table as defined in the arguments. + * This function modifies the passed in args and firstTable objects. + * This process consists out of three parts: + * * Calculating the new column arguments + * * Calculating the new datatable columns + * * Calculating the new rows + * + * If the table is tranposed by multiple columns, this process is repeated on top of the previous transformation. + * + * @param args Arguments for the table visualization + * @param firstTable datatable object containing the actual data + * @param formatters Formatters for all columns to transpose columns by actual display values + */ +export function transposeTable( + args: Args, + firstTable: Datatable, + formatters: Record +) { + args.columns + .filter((columnArgs) => columnArgs.isTransposed) + // start with the inner nested transposed column and work up to preserve column grouping + .reverse() + .forEach(({ columnId: transposedColumnId }) => { + const datatableColumnIndex = firstTable.columns.findIndex((c) => c.id === transposedColumnId); + const datatableColumn = firstTable.columns[datatableColumnIndex]; + const transposedColumnFormatter = formatters[datatableColumn.id]; + const { uniqueValues, uniqueRawValues } = getUniqueValues( + firstTable, + transposedColumnFormatter, + transposedColumnId + ); + const metricsColumnArgs = args.columns.filter((c) => c.transposable); + const bucketsColumnArgs = args.columns.filter( + (c) => !c.transposable && c.columnId !== transposedColumnId + ); + firstTable.columns.splice(datatableColumnIndex, 1); + + transposeColumns( + args, + bucketsColumnArgs, + metricsColumnArgs, + firstTable, + uniqueValues, + uniqueRawValues, + datatableColumn + ); + transposeRows( + firstTable, + bucketsColumnArgs, + formatters, + transposedColumnFormatter, + transposedColumnId, + metricsColumnArgs + ); + }); +} + +function transposeRows( + firstTable: Datatable, + bucketsColumnArgs: Array, + formatters: Record, + transposedColumnFormatter: FieldFormat, + transposedColumnId: string, + metricsColumnArgs: Array +) { + const rowsByBucketColumns: Record = groupRowsByBucketColumns( + firstTable, + bucketsColumnArgs, + formatters + ); + firstTable.rows = mergeRowGroups( + rowsByBucketColumns, + bucketsColumnArgs, + transposedColumnFormatter, + transposedColumnId, + metricsColumnArgs + ); +} + +/** + * Updates column args by adding bucket column args first, then adding transposed metric columns + * grouped by unique value + */ +function updateColumnArgs( + args: Args, + bucketsColumnArgs: Array, + transposedColumnGroups: Array> +) { + args.columns = [...bucketsColumnArgs]; + // add first column from each group, then add second column for each group, ... + transposedColumnGroups[0].forEach((_, index) => { + transposedColumnGroups.forEach((transposedColumnGroup) => { + args.columns.push(transposedColumnGroup[index]); + }); + }); +} + +/** + * Finds all unique values in a column in order of first occurence + * @param table Table to search through + * @param formatter formatter for the column + * @param columnId column + */ +function getUniqueValues(table: Datatable, formatter: FieldFormat, columnId: string) { + const values = new Map(); + table.rows.forEach((row) => { + const rawValue = row[columnId]; + values.set(formatter.convert(row[columnId]), rawValue); + }); + const uniqueValues = [...values.keys()]; + const uniqueRawValues = [...values.values()]; + return { uniqueValues, uniqueRawValues }; +} + +/** + * Calculate transposed column objects of the datatable object and puts them into the datatable. + * Returns args for additional columns grouped by metric + * @param metricColumns + * @param firstTable + * @param uniqueValues + */ +function transposeColumns( + args: Args, + bucketsColumnArgs: Array, + metricColumns: Array, + firstTable: Datatable, + uniqueValues: string[], + uniqueRawValues: unknown[], + transposingDatatableColumn: DatatableColumn +) { + const columnGroups = metricColumns.map((metricColumn) => { + const originalDatatableColumn = firstTable.columns.find((c) => c.id === metricColumn.columnId)!; + const datatableColumns = uniqueValues.map((uniqueValue) => { + return { + ...originalDatatableColumn, + id: getTransposeId(uniqueValue, metricColumn.columnId), + name: `${uniqueValue} ${TRANSPOSE_VISUAL_SEPARATOR} ${originalDatatableColumn.name}`, + }; + }); + firstTable.columns.splice( + firstTable.columns.findIndex((c) => c.id === metricColumn.columnId), + 1, + ...datatableColumns + ); + return uniqueValues.map((uniqueValue, valueIndex) => { + return { + ...metricColumn, + columnId: getTransposeId(uniqueValue, metricColumn.columnId), + originalColumnId: metricColumn.originalColumnId || metricColumn.columnId, + originalName: metricColumn.originalName || originalDatatableColumn.name, + bucketValues: [ + ...(metricColumn.bucketValues || []), + { + originalBucketColumn: transposingDatatableColumn, + value: uniqueRawValues[valueIndex], + }, + ], + }; + }); + }); + updateColumnArgs(args, bucketsColumnArgs, columnGroups); +} + +/** + * Merge groups of rows together by creating separate columns for unique values of the column to transpose by. + */ +function mergeRowGroups( + rowsByBucketColumns: Record, + bucketColumns: ColumnState[], + formatter: FieldFormat, + transposedColumnId: string, + metricColumns: ColumnState[] +) { + return Object.values(rowsByBucketColumns).map((rows) => { + const mergedRow: DatatableRow = {}; + bucketColumns.forEach((c) => { + mergedRow[c.columnId] = rows[0][c.columnId]; + }); + rows.forEach((row) => { + const transposalValue = formatter.convert(row[transposedColumnId]); + metricColumns.forEach((c) => { + mergedRow[getTransposeId(transposalValue, c.columnId)] = row[c.columnId]; + }); + }); + return mergedRow; + }); +} + +/** + * Groups rows of the data table by the values of bucket columns which are not transposed by. + * All rows ending up in a group have the same bucket column value, but have different values of the column to transpose by. + */ +function groupRowsByBucketColumns( + firstTable: Datatable, + bucketColumns: ColumnState[], + formatters: Record +) { + const rowsByBucketColumns: Record = {}; + firstTable.rows.forEach((row) => { + const key = bucketColumns.map((c) => formatters[c.columnId].convert(row[c.columnId])).join(','); + if (!rowsByBucketColumns[key]) { + rowsByBucketColumns[key] = []; + } + rowsByBucketColumns[key].push(row); + }); + return rowsByBucketColumns; +} diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index 92136c557ad38..1848565114dea 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -9,7 +9,13 @@ import { Ast } from '@kbn/interpreter/common'; import { buildExpression } from '../../../../../src/plugins/expressions/public'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks'; import { DatatableVisualizationState, datatableVisualization } from './visualization'; -import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types'; +import { + Operation, + DataType, + FramePublicAPI, + TableSuggestionColumn, + VisualizationDimensionGroupConfig, +} from '../types'; function mockFrame(): FramePublicAPI { return { @@ -132,9 +138,9 @@ describe('Datatable Visualization', () => { expect(suggestions.length).toBeGreaterThan(0); expect(suggestions[0].state.columns).toEqual([ - { columnId: 'col1', width: 123 }, - { columnId: 'col2', hidden: true }, - { columnId: 'col3' }, + { columnId: 'col1', width: 123, isTransposed: false }, + { columnId: 'col2', hidden: true, isTransposed: false }, + { columnId: 'col3', isTransposed: false }, ]); expect(suggestions[0].state.sorting).toEqual({ columnId: 'col1', @@ -226,39 +232,45 @@ describe('Datatable Visualization', () => { }, frame, }).groups - ).toHaveLength(2); + ).toHaveLength(3); }); - it('allows only bucket operations one category', () => { + it('allows only bucket operations for splitting columns and rows', () => { const datasource = createMockDatasource('test'); const frame = mockFrame(); frame.datasourceLayers = { first: datasource.publicAPIMock }; - - const filterOperations = datatableVisualization.getConfiguration({ + const groups = datatableVisualization.getConfiguration({ layerId: 'first', state: { layerId: 'first', columns: [], }, frame, - }).groups[0].filterOperations; + }).groups; - const baseOperation: Operation = { - dataType: 'string', - isBucketed: true, - label: '', - }; - expect(filterOperations({ ...baseOperation })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual(true); - expect(filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false })).toEqual( - false - ); - expect(filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false })).toEqual( - false - ); + function testGroup(group: VisualizationDimensionGroupConfig) { + const baseOperation: Operation = { + dataType: 'string', + isBucketed: true, + label: '', + }; + expect(group.filterOperations({ ...baseOperation })).toEqual(true); + expect(group.filterOperations({ ...baseOperation, dataType: 'number' })).toEqual(true); + expect(group.filterOperations({ ...baseOperation, dataType: 'date' })).toEqual(true); + expect(group.filterOperations({ ...baseOperation, dataType: 'boolean' })).toEqual(true); + expect(group.filterOperations({ ...baseOperation, dataType: 'other' as DataType })).toEqual( + true + ); + expect( + group.filterOperations({ ...baseOperation, dataType: 'date', isBucketed: false }) + ).toEqual(false); + expect( + group.filterOperations({ ...baseOperation, dataType: 'number', isBucketed: false }) + ).toEqual(false); + } + + testGroup(groups[0]); + testGroup(groups[1]); }); it('allows only metric operations in one category', () => { @@ -273,7 +285,7 @@ describe('Datatable Visualization', () => { columns: [], }, frame, - }).groups[1].filterOperations; + }).groups[2].filterOperations; const baseOperation: Operation = { dataType: 'string', @@ -307,7 +319,7 @@ describe('Datatable Visualization', () => { columns: [{ columnId: 'b' }, { columnId: 'c' }], }, frame, - }).groups[1].accessors + }).groups[2].accessors ).toEqual([{ columnId: 'c' }, { columnId: 'b' }]); }); }); @@ -368,7 +380,7 @@ describe('Datatable Visualization', () => { }) ).toEqual({ layerId: 'layer1', - columns: [{ columnId: 'b' }, { columnId: 'c' }, { columnId: 'd' }], + columns: [{ columnId: 'b' }, { columnId: 'c' }, { columnId: 'd', isTransposed: false }], }); }); @@ -382,7 +394,7 @@ describe('Datatable Visualization', () => { }) ).toEqual({ layerId: 'layer1', - columns: [{ columnId: 'b' }, { columnId: 'c' }], + columns: [{ columnId: 'b', isTransposed: false }, { columnId: 'c' }], }); }); }); @@ -419,12 +431,16 @@ describe('Datatable Visualization', () => { columnId: ['c'], hidden: [], width: [], + isTransposed: [], + transposable: [true], alignment: [], }); expect(columnArgs[1].arguments).toEqual({ columnId: ['b'], hidden: [], width: [], + isTransposed: [], + transposable: [true], alignment: [], }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index b7ff23cdb6e35..4094ecee74e1c 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -10,6 +10,7 @@ import { render } from 'react-dom'; import { Ast } from '@kbn/interpreter/common'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { DatatableColumn } from 'src/plugins/expressions/public'; import { SuggestionRequest, Visualization, @@ -23,6 +24,13 @@ export interface ColumnState { columnId: string; width?: number; hidden?: boolean; + isTransposed?: boolean; + // These flags are necessary to transpose columns and map them back later + // They are set automatically and are not user-editable + transposable?: boolean; + originalColumnId?: string; + originalName?: string; + bucketValues?: Array<{ originalBucketColumn: DatatableColumn; value: unknown }>; alignment?: 'left' | 'right' | 'center'; } @@ -108,6 +116,11 @@ export const datatableVisualization: Visualization oldColumnSettings[column.columnId] = column; }); } + const lastTransposedColumnIndex = table.columns.findIndex((c) => + !oldColumnSettings[c.columnId] ? false : !oldColumnSettings[c.columnId]?.isTransposed + ); + const usesTransposing = state?.columns.some((c) => c.isTransposed); + const title = table.changeType === 'unchanged' ? i18n.translate('xpack.lens.datatable.suggestionLabel', { @@ -138,8 +151,9 @@ export const datatableVisualization: Visualization state: { ...(state || {}), layerId: table.layerId, - columns: table.columns.map((col) => ({ + columns: table.columns.map((col, columnIndex) => ({ ...(oldColumnSettings[col.columnId] || {}), + isTransposed: usesTransposing && columnIndex < lastTransposedColumnIndex, columnId: col.columnId, })), }, @@ -166,21 +180,55 @@ export const datatableVisualization: Visualization return { groups: [ { - groupId: 'columns', - groupLabel: i18n.translate('xpack.lens.datatable.breakdown', { - defaultMessage: 'Break down by', + groupId: 'rows', + groupLabel: i18n.translate('xpack.lens.datatable.breakdownRows', { + defaultMessage: 'Split rows', + }), + groupTooltip: i18n.translate('xpack.lens.datatable.breakdownRows.description', { + defaultMessage: + 'Split table rows by field. This is recommended for high cardinality breakdowns.', }), layerId: state.layerId, accessors: sortedColumns - .filter((c) => datasource!.getOperationForColumnId(c)?.isBucketed) + .filter( + (c) => + datasource!.getOperationForColumnId(c)?.isBucketed && + !state.columns.find((col) => col.columnId === c)?.isTransposed + ) .map((accessor) => ({ columnId: accessor, triggerIcon: columnMap[accessor].hidden ? 'invisible' : undefined, })), supportsMoreColumns: true, filterOperations: (op) => op.isBucketed, - dataTestSubj: 'lnsDatatable_column', + dataTestSubj: 'lnsDatatable_rows', + enableDimensionEditor: true, + hideGrouping: true, + nestingOrder: 1, + }, + { + groupId: 'columns', + groupLabel: i18n.translate('xpack.lens.datatable.breakdownColumns', { + defaultMessage: 'Split columns', + }), + groupTooltip: i18n.translate('xpack.lens.datatable.breakdownColumns.description', { + defaultMessage: + "Split metric columns by field. It's recommended to keep the number of columns low to avoid horizontal scrolling.", + }), + layerId: state.layerId, + accessors: sortedColumns + .filter( + (c) => + datasource!.getOperationForColumnId(c)?.isBucketed && + state.columns.find((col) => col.columnId === c)?.isTransposed + ) + .map((accessor) => ({ columnId: accessor })), + supportsMoreColumns: true, + filterOperations: (op) => op.isBucketed, + dataTestSubj: 'lnsDatatable_columns', enableDimensionEditor: true, + hideGrouping: true, + nestingOrder: 0, }, { groupId: 'metrics', @@ -204,13 +252,26 @@ export const datatableVisualization: Visualization }; }, - setDimension({ prevState, columnId }) { - if (prevState.columns.some((column) => column.columnId === columnId)) { - return prevState; + setDimension({ prevState, columnId, groupId, previousColumn }) { + if ( + prevState.columns.some( + (column) => + column.columnId === columnId || (previousColumn && column.columnId === previousColumn) + ) + ) { + return { + ...prevState, + columns: prevState.columns.map((column) => { + if (column.columnId === columnId || column.columnId === previousColumn) { + return { ...column, columnId, isTransposed: groupId === 'columns' }; + } + return column; + }), + }; } return { ...prevState, - columns: [...prevState.columns, { columnId }], + columns: [...prevState.columns, { columnId, isTransposed: groupId === 'columns' }], }; }, removeDimension({ prevState, columnId }) { @@ -268,6 +329,11 @@ export const datatableVisualization: Visualization columnId: [column.columnId], hidden: typeof column.hidden === 'undefined' ? [] : [column.hidden], width: typeof column.width === 'undefined' ? [] : [column.width], + isTransposed: + typeof column.isTransposed === 'undefined' ? [] : [column.isTransposed], + transposable: [ + !datasource!.getOperationForColumnId(column.columnId)?.isBucketed, + ], alignment: typeof column.alignment === 'undefined' ? [] : [column.alignment], }, }, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx index 04ab1318a12e0..8449727a9e79d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx @@ -40,6 +40,7 @@ export function DraggableDimensionButton({ layerIndex, columnId, group, + groups, onDrop, children, layerDatasourceDropProps, @@ -55,6 +56,7 @@ export function DraggableDimensionButton({ dropType?: DropType ) => void; group: VisualizationDimensionGroupConfig; + groups: VisualizationDimensionGroupConfig[]; label: string; children: React.ReactElement; layerDatasource: Datasource; @@ -71,6 +73,7 @@ export function DraggableDimensionButton({ columnId, filterOperations: group.filterOperations, groupId: group.groupId, + dimensionGroups: groups, }); const dropType = dropProps?.dropType; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx index 664e24b989836..a6ccac1427fbf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx @@ -27,6 +27,7 @@ const getAdditionalClassesOnDroppable = (dropType?: string) => { export function EmptyDimensionButton({ group, + groups, layerDatasource, layerDatasourceDropProps, layerId, @@ -45,6 +46,8 @@ export function EmptyDimensionButton({ dropType?: DropType ) => void; group: VisualizationDimensionGroupConfig; + groups: VisualizationDimensionGroupConfig[]; + layerDatasource: Datasource; layerDatasourceDropProps: LayerDatasourceDropProps; }) { @@ -63,6 +66,7 @@ export function EmptyDimensionButton({ columnId: newColumnId, filterOperations: group.filterOperations, groupId: group.groupId, + dimensionGroups: groups, }); const dropType = dropProps?.dropType; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 1d75e873f9b18..14063aea02665 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -8,7 +8,14 @@ import './layer_panel.scss'; import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { + EuiPanel, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../../../native_renderer'; import { StateSetter, Visualization, DraggedOperation, DropType } from '../../../types'; @@ -151,6 +158,8 @@ export function LayerPanel( columnId, layerId: targetLayerId, filterOperations, + dimensionGroups: groups, + groupId, dropType, }); if (dropResult) { @@ -159,6 +168,7 @@ export function LayerPanel( groupId, layerId: targetLayerId, prevState: props.visualizationState, + previousColumn: typeof droppedItem.column === 'string' ? droppedItem.column : undefined, }); if (typeof dropResult === 'object') { @@ -254,7 +264,26 @@ export function LayerPanel( : 'lnsLayerPanel__row lnsLayerPanel__row--notSupportsMoreColumns' } fullWidth - label={
{group.groupLabel}
} + label={ +
+ {group.groupLabel} + {group.groupTooltip && ( + <> + {' '} + + + )} +
+ } labelType="legend" key={group.groupId} isInvalid={isMissing} @@ -281,6 +310,7 @@ export function LayerPanel( accessorIndex={accessorIndex} columnId={columnId} group={group} + groups={groups} groupIndex={groupIndex} key={columnId} layerDatasourceDropProps={layerDatasourceDropProps} @@ -325,6 +355,7 @@ export function LayerPanel( nativeProps={{ ...layerDatasourceConfigProps, columnId: accessorConfig.columnId, + groupId: group.groupId, filterOperations: group.filterOperations, }} /> @@ -338,6 +369,7 @@ export function LayerPanel( ); @@ -424,6 +434,8 @@ export function DimensionEditor(props: DimensionEditorProps) { indexPattern: currentIndexPattern, op: choice.operationType, field: currentIndexPattern.getFieldByName(choice.field), + visualizationGroups: dimensionGroups, + targetGroup: props.groupId, }) ); }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 5eaa798f459e3..a6d2361be21d4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -197,6 +197,7 @@ describe('IndexPatternDimensionEditorPanel', () => { } as unknown) as DataPublicPluginStart, core: {} as CoreSetup, dimensionGroups: [], + groupId: 'a', }; jest.clearAllMocks(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index 12df14f81cb67..82b6434e50aac 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -7,12 +7,13 @@ import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; import { onDrop, getDropProps } from './droppable'; +import { DraggingIdentifier } from '../../drag_drop'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { IndexPatternPrivateState } from '../types'; import { documentField } from '../document_field'; import { OperationMetadata, DropType } from '../../types'; -import { IndexPatternColumn } from '../operations'; +import { IndexPatternColumn, MedianIndexPatternColumn } from '../operations'; import { getFieldByNameFactory } from '../pure_helpers'; const fields = [ @@ -48,6 +49,22 @@ const fields = [ searchable: true, exists: true, }, + { + name: 'src', + displayName: 'src', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, + { + name: 'dest', + displayName: 'dest', + type: 'string', + aggregatable: true, + searchable: true, + exists: true, + }, documentField, ]; @@ -144,6 +161,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columnId: 'col1', layerId: 'first', uniqueLabel: 'stuff', + groupId: 'group1', filterOperations: () => true, storage: {} as IStorageWrapper, uiSettings: {} as IUiSettingsClient, @@ -572,36 +590,458 @@ describe('IndexPatternDimensionEditorPanel', () => { }); }); - it('copies a dimension if dropType is duplicate_in_group, respecting bucket metric order', () => { - const testState = { ...state }; - testState.layers.first = { - indexPatternId: 'foo', - columnOrder: ['col1', 'col2', 'col3'], - columns: { - col1: testState.layers.first.columns.col1, + describe('dimension group aware ordering and copying', () => { + let dragging: DraggingIdentifier; + let testState: IndexPatternPrivateState; + beforeEach(() => { + dragging = { + columnId: 'col2', + groupId: 'b', + layerId: 'first', + id: 'col2', + humanData: { + label: '', + }, + }; + testState = { ...state }; + testState.layers.first = { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + col2: { + label: 'Top values of src', + dataType: 'string', + isBucketed: true, - col2: { - label: 'Top values of src', - dataType: 'string', - isBucketed: true, + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'src', + }, + col3: { + label: 'Top values of dest', + dataType: 'string', + isBucketed: true, - // Private - operationType: 'terms', - params: { - orderBy: { type: 'column', columnId: 'col3' }, - orderDirection: 'desc', - size: 10, + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'desc', + size: 10, + }, + sourceField: 'dest', + }, + col4: { + label: 'Median of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'median', + sourceField: 'bytes', }, - sourceField: 'src', }, - col3: { - label: 'Count', - dataType: 'number', - isBucketed: false, + }; + }); + const dimensionGroups = [ + { + accessors: [], + groupId: 'a', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: () => false, + }, + { + accessors: [{ columnId: 'col1' }, { columnId: 'col2' }, { columnId: 'col3' }], + groupId: 'b', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: () => false, + }, + { + accessors: [{ columnId: 'col4' }], + groupId: 'c', + supportsMoreColumns: true, + hideGrouping: true, + groupLabel: '', + filterOperations: () => false, + }, + ]; - // Private - operationType: 'count', - sourceField: 'Records', + it('respects groups on moving operations from one group to another', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging col2 into newCol in group a + onDrop({ + ...defaultProps, + columnId: 'newCol', + droppedItem: dragging, + state: testState, + groupId: 'a', + dimensionGroups, + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col3', 'col4'], + columns: { + newCol: testState.layers.first.columns.col2, + col1: testState.layers.first.columns.col1, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('respects groups on moving operations from one group to another with overwrite', () => { + // config: + // a: col1, + // b: col2, col3 + // c: col4 + // dragging col3 onto col1 in group a + const draggingCol3 = { + columnId: 'col3', + groupId: 'b', + layerId: 'first', + id: 'col3', + humanData: { + label: '', + }, + }; + onDrop({ + ...defaultProps, + columnId: 'col1', + droppedItem: draggingCol3, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + dropType: 'move_compatible', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'col2', 'col4'], + columns: { + col1: testState.layers.first.columns.col3, + col2: testState.layers.first.columns.col2, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('moves newly created dimension to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col1 into newCol in group b + const draggingCol1 = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { + label: '', + }, + }; + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'move_compatible', + droppedItem: draggingCol1, + state: testState, + groupId: 'b', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col2', 'col3', 'newCol', 'col4'], + columns: { + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('appends the dropped column in the right place when a field is dropped', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + const draggingBytesField = { + field: { type: 'number', name: 'bytes', aggregatable: true }, + indexPatternId: 'foo', + id: 'bar', + humanData: { + label: '', + }, + }; + + onDrop({ + ...defaultProps, + droppedItem: draggingBytesField, + columnId: 'newCol', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: 'a', + dimensionGroups, + dropType: 'field_add', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('appends the dropped column in the right place respecting custom nestingOrder', () => { + // config: + // a: + // b: col1, col2, col3 + // c: col4 + // dragging field into newCol in group a + const draggingBytesField = { + field: { type: 'number', name: 'bytes', aggregatable: true }, + indexPatternId: 'foo', + id: 'bar', + humanData: { + label: '', + }, + }; + + onDrop({ + ...defaultProps, + droppedItem: draggingBytesField, + columnId: 'newCol', + filterOperations: (op: OperationMetadata) => op.dataType === 'number', + groupId: 'a', + dimensionGroups: [ + // a and b are ordered in reverse visually, but nesting order keeps them in place for column order + { ...dimensionGroups[1], nestingOrder: 1 }, + { ...dimensionGroups[0], nestingOrder: 0 }, + { ...dimensionGroups[2] }, + ], + dropType: 'field_add', + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['newCol', 'col1', 'col2', 'col3', 'col4'], + columns: { + newCol: expect.objectContaining({ + dataType: 'number', + sourceField: 'bytes', + }), + col1: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + incompleteColumns: {}, + }, + }, + }); + }); + + it('copies column to the bottom of the current group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // copying col1 within group a + const draggingCol1 = { + columnId: 'col1', + groupId: 'a', + layerId: 'first', + id: 'col1', + humanData: { + label: '', + }, + }; + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'duplicate_in_group', + droppedItem: draggingCol1, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3', 'col4'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: testState.layers.first.columns.col1, + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + col4: testState.layers.first.columns.col4, + }, + }, + }, + }); + }); + + it('moves incompatible column to the bottom of the target group', () => { + // config: + // a: col1 + // b: col2, col3 + // c: col4 + // dragging col4 into newCol in group a + const draggingCol4 = { + columnId: 'col4', + groupId: 'c', + layerId: 'first', + id: 'col4', + humanData: { + label: '', + }, + }; + onDrop({ + ...defaultProps, + columnId: 'newCol', + dropType: 'move_incompatible', + droppedItem: draggingCol4, + state: testState, + groupId: 'a', + dimensionGroups: [ + { ...dimensionGroups[0], accessors: [{ columnId: 'col1' }] }, + { ...dimensionGroups[1], accessors: [{ columnId: 'col2' }, { columnId: 'col3' }] }, + { ...dimensionGroups[2] }, + ], + }); + + expect(setState).toBeCalledTimes(1); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + first: { + ...testState.layers.first, + columnOrder: ['col1', 'newCol', 'col2', 'col3'], + columns: { + col1: testState.layers.first.columns.col1, + newCol: expect.objectContaining({ + sourceField: (testState.layers.first.columns.col4 as MedianIndexPatternColumn) + .sourceField, + }), + col2: testState.layers.first.columns.col2, + col3: testState.layers.first.columns.col3, + }, + incompleteColumns: {}, + }, + }, + }); + }); + }); + + it('if dnd is reorder, it correctly reorders columns', () => { + const testState: IndexPatternPrivateState = { + ...state, + layers: { + first: { + indexPatternId: 'foo', + columnOrder: ['col1', 'col2', 'col3'], + columns: { + col1: { + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + }, + col2: { + label: 'Top values of bar', + dataType: 'number', + isBucketed: true, + operationType: 'terms', + sourceField: 'bar', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 5, + }, + }, + col3: { + operationType: 'avg', + sourceField: 'memory', + label: 'average of memory', + dataType: 'number', + isBucketed: false, + }, + }, }, }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index a7d4774d8aa3d..e846db718f1d3 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -17,6 +17,8 @@ import { insertOrReplaceColumn, deleteColumn, getOperationTypesForField, + getColumnOrder, + reorderByGroups, getOperationDisplay, } from '../operations'; import { mergeLayer } from '../state_helpers'; @@ -191,7 +193,7 @@ function onReorderDrop({ } function onMoveDropToNonCompatibleGroup(props: DropHandlerProps) { - const { columnId, setState, state, layerId, droppedItem } = props; + const { columnId, setState, state, layerId, droppedItem, dimensionGroups, groupId } = props; const layer = state.layers[layerId]; const op = { ...layer.columns[droppedItem.columnId] }; @@ -225,6 +227,8 @@ function onMoveDropToNonCompatibleGroup(props: DropHandlerProps) { const layer = state.layers[layerId]; @@ -258,19 +264,29 @@ function onSameGroupDuplicateDrop({ const newColumnOrder = [...layer.columnOrder]; // put a new bucketed dimension just in front of the metric dimensions, a metric dimension in the back of the array - // TODO this logic does not take into account groups - we probably need to pass the current - // group config to this position to place the column right + // then reorder based on dimension groups if necessary const insertionIndex = op.isBucketed ? newColumnOrder.findIndex((id) => !newColumns[id].isBucketed) : newColumnOrder.length; newColumnOrder.splice(insertionIndex, 0, columnId); + + const newLayer = { + ...layer, + columnOrder: newColumnOrder, + columns: newColumns, + }; + + const updatedColumnOrder = getColumnOrder(newLayer); + + reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); + // Time to replace setState( mergeLayer({ state, layerId, newLayer: { - columnOrder: newColumnOrder, + columnOrder: updatedColumnOrder, columns: newColumns, }, }) @@ -284,6 +300,8 @@ function onMoveDropToCompatibleGroup({ state, layerId, droppedItem, + dimensionGroups, + groupId, }: DropHandlerProps) { const layer = state.layers[layerId]; const op = { ...layer.columns[droppedItem.columnId] }; @@ -296,18 +314,31 @@ function onMoveDropToCompatibleGroup({ const newIndex = newColumnOrder.findIndex((c) => c === columnId); if (newIndex === -1) { - newColumnOrder[oldIndex] = columnId; - } else { + // for newly created columns, remove the old entry and add the last one to the end newColumnOrder.splice(oldIndex, 1); + newColumnOrder.push(columnId); + } else { + // for drop to replace, reuse the same index + newColumnOrder[oldIndex] = columnId; } + const newLayer = { + ...layer, + columnOrder: newColumnOrder, + columns: newColumns, + }; + + const updatedColumnOrder = getColumnOrder(newLayer); + + reorderByGroups(dimensionGroups, groupId, updatedColumnOrder, columnId); // Time to replace setState( mergeLayer({ state, layerId, + newLayer: { - columnOrder: newColumnOrder, + columnOrder: updatedColumnOrder, columns: newColumns, }, }) @@ -316,7 +347,7 @@ function onMoveDropToCompatibleGroup({ } function onFieldDrop(props: DropHandlerProps) { - const { columnId, setState, state, layerId, droppedItem } = props; + const { columnId, setState, state, layerId, droppedItem, groupId, dimensionGroups } = props; const operationsForNewField = getOperationTypesForField( droppedItem.field, @@ -343,6 +374,8 @@ function onFieldDrop(props: DropHandlerProps) { indexPattern: currentIndexPattern, op: fieldIsCompatibleWithCurrent ? selectedColumn.operationType : operationsForNewField[0], field: droppedItem.field, + visualizationGroups: dimensionGroups, + targetGroup: groupId, }); trackUiEvent('drop_onto_dimension'); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx index dddebfbff1466..9ad6a2d20a4c2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.test.tsx @@ -50,6 +50,7 @@ describe('reference editor', () => { savedObjectsClient: {} as SavedObjectsClientContract, http: {} as HttpSetup, data: {} as DataPublicPluginStart, + dimensionGroups: [], }; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index 87be81f66e8e7..353bba9652eff 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -35,6 +35,7 @@ import { FieldSelect } from './field_select'; import { hasField } from '../utils'; import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { VisualizationDimensionGroupConfig } from '../../types'; const operationPanels = getOperationDisplay(); @@ -48,6 +49,7 @@ export interface ReferenceEditorProps { existingFields: IndexPatternPrivateState['existingFields']; dateRange: DateRange; labelAppend?: EuiFormRowProps['labelAppend']; + dimensionGroups: VisualizationDimensionGroupConfig[]; // Services uiSettings: IUiSettingsClient; @@ -68,6 +70,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { selectionStyle, dateRange, labelAppend, + dimensionGroups, ...services } = props; @@ -168,6 +171,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { op: operationType, indexPattern: currentIndexPattern, field: currentIndexPattern.getFieldByName(column.sourceField), + visualizationGroups: dimensionGroups, }) ); } else { @@ -185,6 +189,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { op: operationType, indexPattern: currentIndexPattern, field: possibleField, + visualizationGroups: dimensionGroups, }) ); } @@ -257,7 +262,11 @@ export function ReferenceEditor(props: ReferenceEditorProps) { onChange={(choices) => { if (choices.length === 0) { updateLayer( - deleteColumn({ layer, columnId, indexPattern: currentIndexPattern }) + deleteColumn({ + layer, + columnId, + indexPattern: currentIndexPattern, + }) ); return; } @@ -298,7 +307,13 @@ export function ReferenceEditor(props: ReferenceEditorProps) { incompleteOperation={incompleteOperation} markAllFieldsCompatible={selectionStyle === 'field'} onDeleteColumn={() => { - updateLayer(deleteColumn({ layer, columnId, indexPattern: currentIndexPattern })); + updateLayer( + deleteColumn({ + layer, + columnId, + indexPattern: currentIndexPattern, + }) + ); }} onChoose={(choice) => { updateLayer( @@ -308,6 +323,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) { indexPattern: currentIndexPattern, op: choice.operationType, field: currentIndexPattern.getFieldByName(choice.field), + visualizationGroups: dimensionGroups, }) ); }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index cd7cfc6e8a1b2..64da5e4fb9f74 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -171,7 +171,11 @@ export function getIndexPatternDatasource({ return mergeLayer({ state: prevState, layerId, - newLayer: deleteColumn({ layer: prevState.layers[layerId], columnId, indexPattern }), + newLayer: deleteColumn({ + layer: prevState.layers[layerId], + columnId, + indexPattern, + }), }); }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index e62764cbfef8d..bde07c182555e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -182,6 +182,7 @@ function getExistingLayerSuggestionsForField( field, op: usableAsBucketOperation, columnId: previousDate, + visualizationGroups: [], }), layerId, changeType: 'initial', @@ -197,6 +198,7 @@ function getExistingLayerSuggestionsForField( field, op: usableAsBucketOperation, columnId: generateId(), + visualizationGroups: [], }), layerId, changeType: 'extended', @@ -214,6 +216,7 @@ function getExistingLayerSuggestionsForField( field, columnId: generateId(), op: metricOperation.type, + visualizationGroups: [], }); if (layerWithNewMetric) { suggestions.push( @@ -235,6 +238,7 @@ function getExistingLayerSuggestionsForField( field, columnId: metrics[0], op: metricOperation.type, + visualizationGroups: [], }); if (layerWithReplacedMetric) { suggestions.push( @@ -302,10 +306,12 @@ function createNewLayerWithBucketAggregation( columnId: generateId(), field: documentField, indexPattern, + visualizationGroups: [], }), columnId: generateId(), field, indexPattern, + visualizationGroups: [], }); } @@ -327,10 +333,12 @@ function createNewLayerWithMetricAggregation( columnId: generateId(), field, indexPattern, + visualizationGroups: [], }), columnId: generateId(), field: dateField, indexPattern, + visualizationGroups: [], }); } @@ -483,6 +491,7 @@ function createMetricSuggestion( op: operation.type, field: operation.type === 'count' ? documentField : field, indexPattern, + visualizationGroups: [], }), }); } @@ -525,6 +534,7 @@ function createAlternativeMetricSuggestions( field, columnId, op: possibleOperations[0].type, + visualizationGroups: [], }); if (layerWithNewMetric) { suggestions.push( @@ -558,6 +568,7 @@ function createSuggestionWithDefaultDateHistogram( field: timeField, op: 'date_histogram', columnId: generateId(), + visualizationGroups: [], }), label: i18n.translate('xpack.lens.indexpattern.suggestions.overTimeLabel', { defaultMessage: 'Over time', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts index 1961a4f957d81..4f915160a52a8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts @@ -104,6 +104,7 @@ describe('state_helpers', () => { indexPattern, op: 'missing' as OperationType, columnId: 'none', + visualizationGroups: [], }); }).toThrow(); }); @@ -130,6 +131,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'filters', + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col2', 'col1'] })); }); @@ -157,6 +159,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'date_histogram', field: indexPattern.fields[0], + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col2', 'col1'] })); }); @@ -187,6 +190,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2'] })); }); @@ -225,6 +229,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); @@ -263,6 +268,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'filters', + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); @@ -275,6 +281,7 @@ describe('state_helpers', () => { indexPattern, op: 'terms', field: indexPattern.fields[0], + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -310,6 +317,7 @@ describe('state_helpers', () => { indexPattern, op: 'terms', field: indexPattern.fields[2], + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col2', 'col1'] })); }); @@ -339,6 +347,7 @@ describe('state_helpers', () => { indexPattern, op: 'date_histogram', field: indexPattern.fields[0], + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2'] })); }); @@ -374,6 +383,7 @@ describe('state_helpers', () => { indexPattern, op: 'sum', field: indexPattern.fields[2], + visualizationGroups: [], }) ).toEqual(expect.objectContaining({ columnOrder: ['col1', 'col2', 'col3'] })); }); @@ -395,6 +405,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'testReference' as OperationType, + visualizationGroups: [], }); }).toThrow(); }); @@ -406,6 +417,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'testReference' as OperationType, + visualizationGroups: [], }); expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( @@ -470,6 +482,7 @@ describe('state_helpers', () => { columnId: 'ref1', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -492,6 +505,7 @@ describe('state_helpers', () => { op: 'count', field: documentField, columnId: 'none', + visualizationGroups: [], }); }).toThrow(); }); @@ -503,6 +517,7 @@ describe('state_helpers', () => { indexPattern, op: 'missing' as OperationType, columnId: 'none', + visualizationGroups: [], }); }).toThrow(); }); @@ -539,6 +554,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'date_histogram', field: indexPattern.fields[0], // date + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -572,6 +588,7 @@ describe('state_helpers', () => { indexPattern, op: 'date_histogram', field: indexPattern.fields[0], + visualizationGroups: [], }); }).toThrow(); }); @@ -600,6 +617,7 @@ describe('state_helpers', () => { columnId: 'col1', indexPattern, op: 'terms', + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -636,6 +654,7 @@ describe('state_helpers', () => { indexPattern, op: 'date_histogram', field: indexPattern.fields[1], + visualizationGroups: [], }).columns.col1 ).toEqual( expect.objectContaining({ @@ -671,6 +690,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col1', op: 'filters', + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -706,6 +726,7 @@ describe('state_helpers', () => { columnId: 'col1', op: 'date_histogram', field: indexPattern.fields[0], + visualizationGroups: [], }).columns.col1 ).toEqual( expect.objectContaining({ @@ -740,6 +761,7 @@ describe('state_helpers', () => { columnId: 'col1', op: 'date_histogram', field: indexPattern.fields[1], + visualizationGroups: [], }).columns.col1 ).toEqual( expect.objectContaining({ @@ -775,6 +797,7 @@ describe('state_helpers', () => { columnId: 'col1', op: 'terms', field: indexPattern.fields[0], + visualizationGroups: [], }).columns.col1 ).toEqual( expect.objectContaining({ @@ -819,6 +842,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'avg', field: indexPattern.fields[2], // bytes field + visualizationGroups: [], }); expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith( @@ -876,6 +900,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'willBeReference', op: 'cumulative_sum', + visualizationGroups: [], }); expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith( @@ -925,6 +950,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col1', op: 'testReference' as OperationType, + visualizationGroups: [], }); expect(operationDefinitionMap.testReference.buildColumn).toHaveBeenCalledWith( @@ -1333,6 +1359,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'col2', op: 'filters', + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -1376,6 +1403,7 @@ describe('state_helpers', () => { columnId: 'col2', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ @@ -1414,6 +1442,7 @@ describe('state_helpers', () => { indexPattern, columnId: 'ref', op: 'sum', + visualizationGroups: [], }); expect(result.columnOrder).toEqual(['ref']); @@ -1466,6 +1495,7 @@ describe('state_helpers', () => { columnId: 'col1', op: 'count', field: documentField, + visualizationGroups: [], }) ).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 15acdcd52860a..3a67e8e464323 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -6,7 +6,7 @@ */ import _, { partition } from 'lodash'; -import type { OperationMetadata } from '../../types'; +import type { OperationMetadata, VisualizationDimensionGroupConfig } from '../../types'; import { operationDefinitionMap, operationDefinitions, @@ -25,6 +25,8 @@ interface ColumnChange { columnId: string; indexPattern: IndexPattern; field?: IndexPatternField; + visualizationGroups: VisualizationDimensionGroupConfig[]; + targetGroup?: string; } export function insertOrReplaceColumn(args: ColumnChange): IndexPatternLayer { @@ -42,6 +44,8 @@ export function insertNewColumn({ columnId, field, indexPattern, + visualizationGroups, + targetGroup, }: ColumnChange): IndexPatternLayer { const operationDefinition = operationDefinitionMap[op]; @@ -63,7 +67,13 @@ export function insertNewColumn({ const isBucketed = Boolean(possibleOperation.isBucketed); const addOperationFn = isBucketed ? addBucket : addMetric; return updateDefaultLabels( - addOperationFn(layer, operationDefinition.buildColumn({ ...baseOptions, layer }), columnId), + addOperationFn( + layer, + operationDefinition.buildColumn({ ...baseOptions, layer }), + columnId, + visualizationGroups, + targetGroup + ), indexPattern ); } @@ -97,6 +107,8 @@ export function insertNewColumn({ columnId: newId, op: def.type, indexPattern, + visualizationGroups, + targetGroup, }); } else if (validFields.length === 1) { // Recursively update the layer for each new reference @@ -106,6 +118,8 @@ export function insertNewColumn({ op: def.type, indexPattern, field: validFields[0], + visualizationGroups, + targetGroup, }); } else { tempLayer = { @@ -133,7 +147,9 @@ export function insertNewColumn({ addOperationFn( tempLayer, operationDefinition.buildColumn({ ...baseOptions, layer: tempLayer, referenceIds }), - columnId + columnId, + visualizationGroups, + targetGroup ), indexPattern ); @@ -155,7 +171,9 @@ export function insertNewColumn({ addBucket( layer, operationDefinition.buildColumn({ ...baseOptions, layer, field: invalidField }), - columnId + columnId, + visualizationGroups, + targetGroup ), indexPattern ); @@ -196,7 +214,9 @@ export function insertNewColumn({ addOperationFn( layer, operationDefinition.buildColumn({ ...baseOptions, layer, field }), - columnId + columnId, + visualizationGroups, + targetGroup ), indexPattern ); @@ -208,6 +228,7 @@ export function replaceColumn({ indexPattern, op, field, + visualizationGroups, }: ColumnChange): IndexPatternLayer { const previousColumn = layer.columns[columnId]; if (!previousColumn) { @@ -240,6 +261,7 @@ export function replaceColumn({ previousColumn, op, indexPattern, + visualizationGroups, }); } @@ -297,7 +319,11 @@ export function replaceColumn({ // This logic comes after the transitions because they need to look at previous columns if (previousDefinition.input === 'fullReference') { (previousColumn as ReferenceBasedIndexPatternColumn).references.forEach((id: string) => { - tempLayer = deleteColumn({ layer: tempLayer, columnId: id, indexPattern }); + tempLayer = deleteColumn({ + layer: tempLayer, + columnId: id, + indexPattern, + }); }); } @@ -385,6 +411,7 @@ export function canTransition({ field, indexPattern, filterOperations, + visualizationGroups, }: ColumnChange & { filterOperations: (meta: OperationMetadata) => boolean; }): boolean { @@ -398,7 +425,14 @@ export function canTransition({ } try { - const newLayer = replaceColumn({ layer, columnId, op, field, indexPattern }); + const newLayer = replaceColumn({ + layer, + columnId, + op, + field, + indexPattern, + visualizationGroups, + }); const newDefinition = operationDefinitionMap[op]; const newColumn = newLayer.columns[columnId]; return ( @@ -437,12 +471,14 @@ function applyReferenceTransition({ previousColumn, op, indexPattern, + visualizationGroups, }: { layer: IndexPatternLayer; columnId: string; previousColumn: IndexPatternColumn; op: OperationType; indexPattern: IndexPattern; + visualizationGroups: VisualizationDimensionGroupConfig[]; }): IndexPatternLayer { const operationDefinition = operationDefinitionMap[op]; @@ -499,6 +535,7 @@ function applyReferenceTransition({ columnId: newId, op: validOperations[0].type, indexPattern, + visualizationGroups, }); return newId; } @@ -536,6 +573,7 @@ function applyReferenceTransition({ op: defWithField[0].type, indexPattern, field: indexPattern.getFieldByName(previousColumn.sourceField), + visualizationGroups, }); return newId; } else if (defIgnoringfield.length === 1) { @@ -578,6 +616,7 @@ function applyReferenceTransition({ op: defWithField[0].type, indexPattern, field: previousField, + visualizationGroups, }); return newId; } @@ -633,7 +672,9 @@ function copyCustomLabel(newColumn: IndexPatternColumn, previousColumn: IndexPat function addBucket( layer: IndexPatternLayer, column: IndexPatternColumn, - addedColumnId: string + addedColumnId: string, + visualizationGroups: VisualizationDimensionGroupConfig[], + targetGroup?: string ): IndexPatternLayer { const [buckets, metrics, references] = getExistingColumnGroups(layer); @@ -656,6 +697,7 @@ function addBucket( // they already had, with an extra level of detail. updatedColumnOrder = [...buckets, addedColumnId, ...metrics, ...references]; } + reorderByGroups(visualizationGroups, targetGroup, updatedColumnOrder, addedColumnId); const tempLayer = { ...resetIncomplete(layer, addedColumnId), columns: { ...layer.columns, [addedColumnId]: column }, @@ -664,6 +706,43 @@ function addBucket( return { ...tempLayer, columnOrder: getColumnOrder(tempLayer) }; } +export function reorderByGroups( + visualizationGroups: VisualizationDimensionGroupConfig[], + targetGroup: string | undefined, + updatedColumnOrder: string[], + addedColumnId: string +) { + const hidesColumnGrouping = + targetGroup && visualizationGroups.find((group) => group.groupId === targetGroup)?.hideGrouping; + + // if column grouping is disabled, keep bucket aggregations in the same order as the groups + // if grouping is known + if (hidesColumnGrouping) { + const orderedVisualizationGroups = [...visualizationGroups]; + orderedVisualizationGroups.sort((group1, group2) => { + if (typeof group1.nestingOrder === undefined) { + return -1; + } + if (typeof group2.nestingOrder === undefined) { + return 1; + } + return group1.nestingOrder! - group2.nestingOrder!; + }); + const columnGroupIndex: Record = {}; + updatedColumnOrder.forEach((columnId) => { + columnGroupIndex[columnId] = orderedVisualizationGroups.findIndex( + (group) => + (columnId === addedColumnId && group.groupId === targetGroup) || + group.accessors.some((acc) => acc.columnId === columnId) + ); + }); + + updatedColumnOrder.sort((a, b) => { + return columnGroupIndex[a] - columnGroupIndex[b]; + }); + } +} + function addMetric( layer: IndexPatternLayer, column: IndexPatternColumn, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index e42ca36e5cfc3..3b47792af4254 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -264,6 +264,7 @@ interface SharedDimensionProps { export type DatasourceDimensionProps = SharedDimensionProps & { layerId: string; columnId: string; + groupId: string; onRemove?: (accessor: string) => void; state: T; activeData?: Record; @@ -308,9 +309,11 @@ export function isDraggedOperation( export type DatasourceDimensionDropProps = SharedDimensionProps & { layerId: string; + groupId: string; columnId: string; state: T; setState: StateSetter; + dimensionGroups: VisualizationDimensionGroupConfig[]; }; export type DatasourceDimensionDropHandlerProps = DatasourceDimensionDropProps & { @@ -388,6 +391,7 @@ export interface AccessorConfig { export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; + groupTooltip?: string; /** ID is passed back to visualization. For example, `x` */ groupId: string; @@ -402,6 +406,10 @@ export type VisualizationDimensionGroupConfig = SharedDimensionProps & { * will render the extra tab for the dimension editor */ enableDimensionEditor?: boolean; + // if the visual order of dimension groups diverges from the intended nesting order, this property specifies the position of + // this dimension group in the hierarchy. If not specified, the position of the dimension in the array is used. specified nesting + // orders are always higher in the hierarchy than non-specified ones. + nestingOrder?: number; }; interface VisualizationDimensionChangeProps { @@ -592,7 +600,9 @@ export interface Visualization { * The frame is telling the visualization to update or set a dimension based on user interaction * groupId is coming from the groupId provided in getConfiguration */ - setDimension: (props: VisualizationDimensionChangeProps & { groupId: string }) => T; + setDimension: ( + props: VisualizationDimensionChangeProps & { groupId: string; previousColumn?: string } + ) => T; /** * The frame is telling the visualization to remove a dimension. The visualization needs to * look at its internal state to determine which dimension is being affected. diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0cbaf1a7921c2..519448c269852 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -11850,7 +11850,6 @@ "xpack.lens.configure.invalidConfigTooltipClick": "詳細はクリックしてください。", "xpack.lens.customBucketContainer.dragToReorder": "ドラッグして並べ替え", "xpack.lens.dataPanelWrapper.switchDatasource": "データソースに切り替える", - "xpack.lens.datatable.breakdown": "内訳の基準", "xpack.lens.datatable.conjunctionSign": " & ", "xpack.lens.datatable.expressionHelpLabel": "データベースレンダー", "xpack.lens.datatable.label": "データテーブル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f11aa3fc3da6e..6920758caa10d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -12007,7 +12007,6 @@ "xpack.lens.configure.invalidConfigTooltipClick": "单击了解更多详情。", "xpack.lens.customBucketContainer.dragToReorder": "拖动以重新排序", "xpack.lens.dataPanelWrapper.switchDatasource": "切换到数据源", - "xpack.lens.datatable.breakdown": "细分方式", "xpack.lens.datatable.conjunctionSign": " & ", "xpack.lens.datatable.expressionHelpLabel": "数据表呈现器", "xpack.lens.datatable.label": "数据表", diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index 5e4557057212f..519d33a947888 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -30,32 +30,32 @@ export default function ({ getPageObjects }: FtrProviderContext) { await PageObjects.lens.dragFieldToDimensionTrigger( 'clientip', - 'lnsDatatable_column > lns-dimensionTrigger' + 'lnsDatatable_rows > lns-dimensionTrigger' ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column')).to.eql( + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_rows')).to.eql( 'Top values of clientip' ); await PageObjects.lens.dragFieldToDimensionTrigger( 'bytes', - 'lnsDatatable_column > lns-empty-dimension' + 'lnsDatatable_rows > lns-empty-dimension' ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 1)).to.eql( + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_rows', 1)).to.eql( 'bytes' ); await PageObjects.lens.dragFieldToDimensionTrigger( '@message.raw', - 'lnsDatatable_column > lns-empty-dimension' + 'lnsDatatable_rows > lns-empty-dimension' ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 2)).to.eql( + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_rows', 2)).to.eql( 'Top values of @message.raw' ); }); it('should reorder the elements for the table', async () => { - await PageObjects.lens.reorderDimensions('lnsDatatable_column', 3, 1); + await PageObjects.lens.reorderDimensions('lnsDatatable_rows', 3, 1); await PageObjects.lens.waitForVisualization(); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([ + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_rows')).to.eql([ 'Top values of @message.raw', 'Top values of clientip', 'bytes', diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 946f3a1dcba95..62e9b39d208b5 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); await PageObjects.lens.switchToVisualization('lnsDatatable'); - await PageObjects.lens.removeDimension('lnsDatatable_column'); + await PageObjects.lens.removeDimension('lnsDatatable_rows'); await PageObjects.lens.switchToVisualization('bar_stacked'); await PageObjects.lens.configureDimension({ @@ -446,7 +446,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.switchToVisualization('lnsDatatable'); await PageObjects.lens.configureDimension({ - dimension: 'lnsDatatable_column > lns-empty-dimension', + dimension: 'lnsDatatable_rows > lns-empty-dimension', operation: 'date_histogram', field: '@timestamp', }); diff --git a/x-pack/test/functional/apps/lens/table.ts b/x-pack/test/functional/apps/lens/table.ts index 2d96458523cb6..f0f3ce27f4c31 100644 --- a/x-pack/test/functional/apps/lens/table.ts +++ b/x-pack/test/functional/apps/lens/table.ts @@ -59,16 +59,39 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('@timestamp per 3 hours'); expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal('Average of bytes'); - await PageObjects.lens.toggleColumnVisibility('lnsDatatable_column > lns-dimensionTrigger'); + await PageObjects.lens.toggleColumnVisibility('lnsDatatable_rows > lns-dimensionTrigger'); expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('@timestamp per 3 hours'); expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('Average of bytes'); - await PageObjects.lens.toggleColumnVisibility('lnsDatatable_column > lns-dimensionTrigger'); + await PageObjects.lens.toggleColumnVisibility('lnsDatatable_rows > lns-dimensionTrigger'); expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('Top values of ip'); expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal('@timestamp per 3 hours'); expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal('Average of bytes'); }); + + it('should allow to transpose columns', async () => { + await PageObjects.lens.dragDimensionToDimension( + 'lnsDatatable_rows > lns-dimensionTrigger', + 'lnsDatatable_columns > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDatatableHeaderText(0)).to.equal('@timestamp per 3 hours'); + expect(await PageObjects.lens.getDatatableHeaderText(1)).to.equal( + '169.228.188.120 › Average of bytes' + ); + expect(await PageObjects.lens.getDatatableHeaderText(2)).to.equal( + '78.83.247.30 › Average of bytes' + ); + expect(await PageObjects.lens.getDatatableHeaderText(3)).to.equal( + '226.82.228.233 › Average of bytes' + ); + }); + + it('should allow to sort by transposed columns', async () => { + await PageObjects.lens.changeTableSortingBy(2, 'ascending'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDatatableCellText(0, 2)).to.eql('17,246'); + }); }); } From dfb4eac520e292cf3361330f675d92e963b99c80 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 11 Mar 2021 14:08:48 +0000 Subject: [PATCH 26/26] [ML] Adding support for saved object based ml modules (#92855) * [ML] Adding support for saved object based ml modules * updating icon mapping * cleaning up code * missed private variable * removing mappings json file * renaming module id * updating test * removing unrelated file * type clean up * changing logo type * changes based on review * removing fleet changes * updating type guards * fixing list module return type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ml/common/types/modules.ts | 16 +- .../plugins/ml/common/types/saved_objects.ts | 1 + .../models/data_recognizer/data_recognizer.ts | 353 ++++++++++-------- .../ml/server/saved_objects/mappings.json | 25 -- .../ml/server/saved_objects/mappings.ts | 90 +++++ .../ml/server/saved_objects/saved_objects.ts | 16 +- 6 files changed, 319 insertions(+), 182 deletions(-) delete mode 100644 x-pack/plugins/ml/server/saved_objects/mappings.json create mode 100644 x-pack/plugins/ml/server/saved_objects/mappings.ts diff --git a/x-pack/plugins/ml/common/types/modules.ts b/x-pack/plugins/ml/common/types/modules.ts index 7c9623d3e68ec..617000d017025 100644 --- a/x-pack/plugins/ml/common/types/modules.ts +++ b/x-pack/plugins/ml/common/types/modules.ts @@ -16,6 +16,7 @@ export interface ModuleJob { export interface ModuleDatafeed { id: string; + job_id: string; config: Omit; } @@ -48,7 +49,8 @@ export interface Module { title: string; description: string; type: string; - logoFile: string; + logoFile?: string; + logo?: Logo; defaultIndexPattern: string; query: any; jobs: ModuleJob[]; @@ -56,6 +58,18 @@ export interface Module { kibana: KibanaObjects; } +export interface FileBasedModule extends Omit { + jobs: Array<{ file: string; id: string }>; + datafeeds: Array<{ file: string; job_id: string; id: string }>; + kibana: { + search: Array<{ file: string; id: string }>; + visualization: Array<{ file: string; id: string }>; + dashboard: Array<{ file: string; id: string }>; + }; +} + +export type Logo = { icon: string } | null; + export interface ResultItem { id: string; success?: boolean; diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts index f40eefa2167c9..c90707d39ab14 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -7,6 +7,7 @@ export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export const ML_SAVED_OBJECT_TYPE = 'ml-job'; +export const ML_MODULE_SAVED_OBJECT_TYPE = 'ml-module'; export interface SavedObjectResult { [jobId: string]: { success: boolean; error?: any }; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index a1fac92d45b4e..4e99330610fca 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -9,6 +9,7 @@ import fs from 'fs'; import Boom from '@hapi/boom'; import numeral from '@elastic/numeral'; import { KibanaRequest, IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; + import moment from 'moment'; import { IndexPatternAttributes } from 'src/plugins/data/server'; import { merge } from 'lodash'; @@ -16,12 +17,15 @@ import { AnalysisLimits } from '../../../common/types/anomaly_detection_jobs'; import { getAuthorizationHeader } from '../../lib/request_authorization'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import type { MlClient } from '../../lib/ml_client'; +import { ML_MODULE_SAVED_OBJECT_TYPE } from '../../../common/types/saved_objects'; import { KibanaObjects, KibanaObjectConfig, ModuleDatafeed, ModuleJob, Module, + FileBasedModule, + Logo, JobOverride, DatafeedOverride, GeneralJobsOverride, @@ -45,7 +49,10 @@ import { jobServiceProvider } from '../job_service'; import { resultsServiceProvider } from '../results_service'; import { JobExistResult, JobStat } from '../../../common/types/data_recognizer'; import { MlJobsStatsResponse } from '../../../common/types/job_service'; +import { Datafeed } from '../../../common/types/anomaly_detection_jobs'; import { JobSavedObjectService } from '../../saved_objects'; +import { isDefined } from '../../../common/types/guards'; +import { isPopulatedObject } from '../../../common/util/object_utils'; const ML_DIR = 'ml'; const KIBANA_DIR = 'kibana'; @@ -57,26 +64,18 @@ export const SAVED_OBJECT_TYPES = { VISUALIZATION: 'visualization', }; -interface RawModuleConfig { - id: string; - title: string; - description: string; - type: string; - logoFile: string; - defaultIndexPattern: string; - query: any; - jobs: Array<{ file: string; id: string }>; - datafeeds: Array<{ file: string; job_id: string; id: string }>; - kibana: { - search: Array<{ file: string; id: string }>; - visualization: Array<{ file: string; id: string }>; - dashboard: Array<{ file: string; id: string }>; - }; +function isModule(arg: unknown): arg is Module { + return isPopulatedObject(arg) && Array.isArray(arg.jobs) && arg.jobs[0]?.config !== undefined; +} + +function isFileBasedModule(arg: unknown): arg is FileBasedModule { + return isPopulatedObject(arg) && Array.isArray(arg.jobs) && arg.jobs[0]?.file !== undefined; } interface Config { - dirName: any; - json: RawModuleConfig; + dirName?: string; + module: FileBasedModule | Module; + isSavedObject: boolean; } export interface RecognizeResult { @@ -84,7 +83,7 @@ export interface RecognizeResult { title: string; query: any; description: string; - logo: { icon: string } | null; + logo: Logo; } interface ObjectExistResult { @@ -125,7 +124,7 @@ export class DataRecognizer { /** * List of the module jobs that require model memory estimation */ - jobsForModelMemoryEstimation: Array<{ job: ModuleJob; query: any }> = []; + private _jobsForModelMemoryEstimation: Array<{ job: ModuleJob; query: any }> = []; constructor( mlClusterClient: IScopedClusterClient, @@ -146,7 +145,7 @@ export class DataRecognizer { } // list all directories under the given directory - async listDirs(dirName: string): Promise { + private async _listDirs(dirName: string): Promise { const dirs: string[] = []; return new Promise((resolve, reject) => { fs.readdir(dirName, (err, fileNames) => { @@ -164,7 +163,7 @@ export class DataRecognizer { }); } - async readFile(fileName: string): Promise { + private async _readFile(fileName: string): Promise { return new Promise((resolve, reject) => { fs.readFile(fileName, 'utf-8', (err, content) => { if (err) { @@ -176,14 +175,14 @@ export class DataRecognizer { }); } - async loadManifestFiles(): Promise { + private async _loadConfigs(): Promise { const configs: Config[] = []; - const dirs = await this.listDirs(this._modulesDir); + const dirs = await this._listDirs(this._modulesDir); await Promise.all( dirs.map(async (dir) => { let file: string | undefined; try { - file = await this.readFile(`${this._modulesDir}/${dir}/manifest.json`); + file = await this._readFile(`${this._modulesDir}/${dir}/manifest.json`); } catch (error) { mlLog.warn(`Data recognizer skipping folder ${dir} as manifest.json cannot be read`); } @@ -192,7 +191,8 @@ export class DataRecognizer { try { configs.push({ dirName: dir, - json: JSON.parse(file), + module: JSON.parse(file), + isSavedObject: false, }); } catch (error) { mlLog.warn(`Data recognizer error parsing ${dir}/manifest.json. ${error}`); @@ -201,26 +201,40 @@ export class DataRecognizer { }) ); - return configs; + const savedObjectConfigs = (await this._loadSavedObjectModules()).map((module) => ({ + module, + isSavedObject: true, + })); + + return [...configs, ...savedObjectConfigs]; + } + + private async _loadSavedObjectModules() { + const jobs = await this._savedObjectsClient.find({ + type: ML_MODULE_SAVED_OBJECT_TYPE, + perPage: 10000, + }); + + return jobs.saved_objects.map((o) => o.attributes); } // get the manifest.json file for a specified id, e.g. "nginx" - async getManifestFile(id: string) { - const manifestFiles = await this.loadManifestFiles(); - return manifestFiles.find((i) => i.json.id === id); + private async _findConfig(id: string) { + const configs = await this._loadConfigs(); + return configs.find((i) => i.module.id === id); } // called externally by an endpoint - async findMatches(indexPattern: string): Promise { - const manifestFiles = await this.loadManifestFiles(); + public async findMatches(indexPattern: string): Promise { + const manifestFiles = await this._loadConfigs(); const results: RecognizeResult[] = []; await Promise.all( manifestFiles.map(async (i) => { - const moduleConfig = i.json; + const moduleConfig = i.module; let match = false; try { - match = await this.searchForFields(moduleConfig, indexPattern); + match = await this._searchForFields(moduleConfig, indexPattern); } catch (error) { mlLog.warn( `Data recognizer error running query defined for module ${moduleConfig.id}. ${error}` @@ -228,13 +242,15 @@ export class DataRecognizer { } if (match === true) { - let logo = null; - if (moduleConfig.logoFile) { + let logo: Logo = null; + if (moduleConfig.logo) { + logo = moduleConfig.logo; + } else if (moduleConfig.logoFile) { try { - logo = await this.readFile( + const logoFile = await this._readFile( `${this._modulesDir}/${i.dirName}/${moduleConfig.logoFile}` ); - logo = JSON.parse(logo); + logo = JSON.parse(logoFile); } catch (e) { logo = null; } @@ -255,7 +271,7 @@ export class DataRecognizer { return results; } - async searchForFields(moduleConfig: RawModuleConfig, indexPattern: string) { + private async _searchForFields(moduleConfig: FileBasedModule | Module, indexPattern: string) { if (moduleConfig.query === undefined) { return false; } @@ -275,29 +291,34 @@ export class DataRecognizer { return body.hits.total.value > 0; } - async listModules() { - const manifestFiles = await this.loadManifestFiles(); - const ids = manifestFiles.map(({ json }) => json.id).sort((a, b) => a.localeCompare(b)); // sort as json files are read from disk and could be in any order. + public async listModules() { + const manifestFiles = await this._loadConfigs(); + manifestFiles.sort((a, b) => a.module.id.localeCompare(b.module.id)); // sort as json files are read from disk and could be in any order. - const modules = []; - for (let i = 0; i < ids.length; i++) { - const module = await this.getModule(ids[i]); - modules.push(module); + const configs: Array = []; + for (const config of manifestFiles) { + if (config.isSavedObject) { + configs.push(config.module); + } else { + configs.push(await this.getModule(config.module.id)); + } } - return modules; + // casting return as Module[] so not to break external plugins who rely on this function + // once FileBasedModules are removed this function will only deal with Modules + return configs as Module[]; } // called externally by an endpoint // supplying an optional prefix will add the prefix // to the job and datafeed configs - async getModule(id: string, prefix = ''): Promise { - let manifestJSON: RawModuleConfig | null = null; + public async getModule(id: string, prefix = ''): Promise { + let module: FileBasedModule | Module | null = null; let dirName: string | null = null; - const manifestFile = await this.getManifestFile(id); - if (manifestFile !== undefined) { - manifestJSON = manifestFile.json; - dirName = manifestFile.dirName; + const config = await this._findConfig(id); + if (config !== undefined) { + module = config.module; + dirName = config.dirName ?? null; } else { throw Boom.notFound(`Module with the id "${id}" not found`); } @@ -306,81 +327,102 @@ export class DataRecognizer { const datafeeds: ModuleDatafeed[] = []; const kibana: KibanaObjects = {}; // load all of the job configs - await Promise.all( - manifestJSON.jobs.map(async (job) => { + if (isModule(module)) { + const tempJobs: ModuleJob[] = module.jobs.map((j) => ({ + id: `${prefix}${j.id}`, + config: j.config, + })); + jobs.push(...tempJobs); + const tempDatafeeds: ModuleDatafeed[] = module.datafeeds.map((d) => { + const jobId = `${prefix}${d.job_id}`; + return { + id: prefixDatafeedId(d.id, prefix), + job_id: jobId, + config: { + ...d.config, + job_id: jobId, + }, + }; + }); + datafeeds.push(...tempDatafeeds); + } else if (isFileBasedModule(module)) { + const tempJobs = module.jobs.map(async (job) => { try { - const jobConfig = await this.readFile( + const jobConfig = await this._readFile( `${this._modulesDir}/${dirName}/${ML_DIR}/${job.file}` ); // use the file name for the id - jobs.push({ + return { id: `${prefix}${job.id}`, config: JSON.parse(jobConfig), - }); + }; } catch (error) { mlLog.warn( `Data recognizer error loading config for job ${job.id} for module ${id}. ${error}` ); } - }) - ); + }); + jobs.push(...(await Promise.all(tempJobs)).filter(isDefined)); - // load all of the datafeed configs - await Promise.all( - manifestJSON.datafeeds.map(async (datafeed) => { + // load all of the datafeed configs + const tempDatafeed = module.datafeeds.map(async (datafeed) => { try { - const datafeedConfig = await this.readFile( + const datafeedConfigString = await this._readFile( `${this._modulesDir}/${dirName}/${ML_DIR}/${datafeed.file}` ); - const config = JSON.parse(datafeedConfig); - // use the job id from the manifestFile - config.job_id = `${prefix}${datafeed.job_id}`; + const datafeedConfig = JSON.parse(datafeedConfigString) as Datafeed; + // use the job id from the module + datafeedConfig.job_id = `${prefix}${datafeed.job_id}`; - datafeeds.push({ + return { id: prefixDatafeedId(datafeed.id, prefix), - config, - }); + job_id: datafeedConfig.job_id, + config: datafeedConfig, + }; } catch (error) { mlLog.warn( `Data recognizer error loading config for datafeed ${datafeed.id} for module ${id}. ${error}` ); } - }) - ); + }); + datafeeds.push(...(await Promise.all(tempDatafeed)).filter(isDefined)); + } // load all of the kibana saved objects - if (manifestJSON.kibana !== undefined) { - const kKeys = Object.keys(manifestJSON.kibana) as Array; + if (module.kibana !== undefined) { + const kKeys = Object.keys(module.kibana) as Array; await Promise.all( kKeys.map(async (key) => { kibana[key] = []; - await Promise.all( - manifestJSON!.kibana[key].map(async (obj) => { - try { - const kConfig = await this.readFile( - `${this._modulesDir}/${dirName}/${KIBANA_DIR}/${key}/${obj.file}` - ); - // use the file name for the id - const kId = obj.file.replace('.json', ''); - const config = JSON.parse(kConfig); - kibana[key]!.push({ - id: kId, - title: config.title, - config, - }); - } catch (error) { - mlLog.warn( - `Data recognizer error loading config for ${key} ${obj.id} for module ${id}. ${error}` - ); - } - }) - ); + if (isFileBasedModule(module)) { + await Promise.all( + module.kibana[key].map(async (obj) => { + try { + const kConfigString = await this._readFile( + `${this._modulesDir}/${dirName}/${KIBANA_DIR}/${key}/${obj.file}` + ); + // use the file name for the id + const kId = obj.file.replace('.json', ''); + const kConfig = JSON.parse(kConfigString); + kibana[key]!.push({ + id: kId, + title: kConfig.title, + config: kConfig, + }); + } catch (error) { + mlLog.warn( + `Data recognizer error loading config for ${key} ${obj.id} for module ${id}. ${error}` + ); + } + }) + ); + } }) ); } return { - ...manifestJSON, + ...module, jobs, datafeeds, kibana, @@ -391,7 +433,7 @@ export class DataRecognizer { // takes a module config id, an optional jobPrefix and the request object // creates all of the jobs, datafeeds and savedObjects listed in the module config. // if any of the savedObjects already exist, they will not be overwritten. - async setup( + public async setup( moduleId: string, jobPrefix?: string, groups?: string[], @@ -417,11 +459,11 @@ export class DataRecognizer { this._indexPatternName = indexPatternName === undefined ? moduleConfig.defaultIndexPattern : indexPatternName; - this._indexPatternId = await this.getIndexPatternId(this._indexPatternName); + this._indexPatternId = await this._getIndexPatternId(this._indexPatternName); // the module's jobs contain custom URLs which require an index patten id // but there is no corresponding index pattern, throw an error - if (this._indexPatternId === undefined && this.doJobUrlsContainIndexPatternId(moduleConfig)) { + if (this._indexPatternId === undefined && this._doJobUrlsContainIndexPatternId(moduleConfig)) { throw Boom.badRequest( `Module's jobs contain custom URLs which require a kibana index pattern (${this._indexPatternName}) which cannot be found.` ); @@ -431,7 +473,7 @@ export class DataRecognizer { // but there is no corresponding index pattern, throw an error if ( this._indexPatternId === undefined && - this.doSavedObjectsContainIndexPatternId(moduleConfig) + this._doSavedObjectsContainIndexPatternId(moduleConfig) ) { throw Boom.badRequest( `Module's saved objects contain custom URLs which require a kibana index pattern (${this._indexPatternName}) which cannot be found.` @@ -439,23 +481,23 @@ export class DataRecognizer { } // create an empty results object - const results = this.createResultsTemplate(moduleConfig); + const results = this._createResultsTemplate(moduleConfig); const saveResults: SaveResults = { jobs: [] as JobResponse[], datafeeds: [] as DatafeedResponse[], savedObjects: [] as KibanaObjectResponse[], }; - this.jobsForModelMemoryEstimation = moduleConfig.jobs.map((job) => ({ + this._jobsForModelMemoryEstimation = moduleConfig.jobs.map((job) => ({ job, query: moduleConfig.datafeeds.find((d) => d.config.job_id === job.id)?.config.query ?? null, })); this.applyJobConfigOverrides(moduleConfig, jobOverrides, jobPrefix); this.applyDatafeedConfigOverrides(moduleConfig, datafeedOverrides, jobPrefix); - this.updateDatafeedIndices(moduleConfig); - this.updateJobUrlIndexPatterns(moduleConfig); - await this.updateModelMemoryLimits(moduleConfig, estimateModelMemory, start, end); + this._updateDatafeedIndices(moduleConfig); + this._updateJobUrlIndexPatterns(moduleConfig); + await this._updateModelMemoryLimits(moduleConfig, estimateModelMemory, start, end); // create the jobs if (moduleConfig.jobs && moduleConfig.jobs.length) { @@ -468,7 +510,7 @@ export class DataRecognizer { if (useDedicatedIndex === true) { moduleConfig.jobs.forEach((job) => (job.config.results_index_name = job.id)); } - saveResults.jobs = await this.saveJobs(moduleConfig.jobs, applyToAllSpaces); + saveResults.jobs = await this._saveJobs(moduleConfig.jobs, applyToAllSpaces); } // create the datafeeds @@ -478,7 +520,7 @@ export class DataRecognizer { df.config.query = query; }); } - saveResults.datafeeds = await this.saveDatafeeds(moduleConfig.datafeeds); + saveResults.datafeeds = await this._saveDatafeeds(moduleConfig.datafeeds); if (startDatafeed) { const savedDatafeeds = moduleConfig.datafeeds.filter((df) => { @@ -486,7 +528,7 @@ export class DataRecognizer { return datafeedResult !== undefined && datafeedResult.success === true; }); - const startResults = await this.startDatafeeds(savedDatafeeds, start, end); + const startResults = await this._startDatafeeds(savedDatafeeds, start, end); saveResults.datafeeds.forEach((df) => { const startedDatafeed = startResults[df.id]; if (startedDatafeed !== undefined) { @@ -503,26 +545,26 @@ export class DataRecognizer { // create the savedObjects if (moduleConfig.kibana) { // update the saved objects with the index pattern id - this.updateSavedObjectIndexPatterns(moduleConfig); + this._updateSavedObjectIndexPatterns(moduleConfig); - const savedObjects = await this.createSavedObjectsToSave(moduleConfig); + const savedObjects = await this._createSavedObjectsToSave(moduleConfig); // update the exists flag in the results - this.updateKibanaResults(results.kibana, savedObjects); + this._updateKibanaResults(results.kibana, savedObjects); // create the savedObjects try { - saveResults.savedObjects = await this.saveKibanaObjects(savedObjects); + saveResults.savedObjects = await this._saveKibanaObjects(savedObjects); } catch (error) { // only one error is returned for the bulk create saved object request // so populate every saved object with the same error. - this.populateKibanaResultErrors(results.kibana, error.output?.payload); + this._populateKibanaResultErrors(results.kibana, error.output?.payload); } } // merge all the save results - this.updateResults(results, saveResults); + this._updateResults(results, saveResults); return results; } - async dataRecognizerJobsExist(moduleId: string): Promise { + public async dataRecognizerJobsExist(moduleId: string): Promise { const results = {} as JobExistResult; // Load the module with the specified ID and check if the jobs @@ -573,7 +615,7 @@ export class DataRecognizer { return results; } - async loadIndexPatterns() { + private async _loadIndexPatterns() { return await this._savedObjectsClient.find({ type: 'index-pattern', perPage: 1000, @@ -581,9 +623,9 @@ export class DataRecognizer { } // returns a id based on an index pattern name - async getIndexPatternId(name: string) { + private async _getIndexPatternId(name: string) { try { - const indexPatterns = await this.loadIndexPatterns(); + const indexPatterns = await this._loadIndexPatterns(); if (indexPatterns === undefined || indexPatterns.saved_objects === undefined) { return; } @@ -598,9 +640,9 @@ export class DataRecognizer { // create a list of objects which are used to save the savedObjects. // each has an exists flag and those which do not already exist // contain a savedObject object which is sent to the server to save - async createSavedObjectsToSave(moduleConfig: Module) { + private async _createSavedObjectsToSave(moduleConfig: Module) { // first check if the saved objects already exist. - const savedObjectExistResults = await this.checkIfSavedObjectsExist(moduleConfig.kibana); + const savedObjectExistResults = await this._checkIfSavedObjectsExist(moduleConfig.kibana); // loop through the kibanaSaveResults and update Object.keys(moduleConfig.kibana).forEach((type) => { // type e.g. dashboard, search ,visualization @@ -624,7 +666,7 @@ export class DataRecognizer { } // update the exists flags in the kibana results - updateKibanaResults( + private _updateKibanaResults( kibanaSaveResults: DataRecognizerConfigResponse['kibana'], objectExistResults: ObjectExistResult[] ) { @@ -640,7 +682,7 @@ export class DataRecognizer { // add an error object to every kibana saved object, // if it doesn't already exist. - populateKibanaResultErrors( + private _populateKibanaResultErrors( kibanaSaveResults: DataRecognizerConfigResponse['kibana'], error: any ) { @@ -661,11 +703,13 @@ export class DataRecognizer { // load existing savedObjects for each type and compare to find out if // items with the same id already exist. // returns a flat list of objects with exists flags set - async checkIfSavedObjectsExist(kibanaObjects: KibanaObjects): Promise { + private async _checkIfSavedObjectsExist( + kibanaObjects: KibanaObjects + ): Promise { const types = Object.keys(kibanaObjects); const results: ObjectExistResponse[][] = await Promise.all( types.map(async (type) => { - const existingObjects = await this.loadExistingSavedObjects(type); + const existingObjects = await this._loadExistingSavedObjects(type); return kibanaObjects[type]!.map((obj) => { const existingObject = existingObjects.saved_objects.find( (o) => o.attributes && o.attributes.title === obj.title @@ -683,13 +727,13 @@ export class DataRecognizer { } // find all existing savedObjects for a given type - loadExistingSavedObjects(type: string) { + private _loadExistingSavedObjects(type: string) { // TODO: define saved object type return this._savedObjectsClient.find({ type, perPage: 1000 }); } // save the savedObjects if they do not exist already - async saveKibanaObjects(objectExistResults: ObjectExistResponse[]) { + private async _saveKibanaObjects(objectExistResults: ObjectExistResponse[]) { let results = { saved_objects: [] as any[] }; const filteredSavedObjects = objectExistResults .filter((o) => o.exists === false) @@ -710,13 +754,16 @@ export class DataRecognizer { // save the jobs. // if any fail (e.g. it already exists), catch the error and mark the result // as success: false - async saveJobs(jobs: ModuleJob[], applyToAllSpaces: boolean = false): Promise { + private async _saveJobs( + jobs: ModuleJob[], + applyToAllSpaces: boolean = false + ): Promise { const resp = await Promise.all( jobs.map(async (job) => { const jobId = job.id; try { job.id = jobId; - await this.saveJob(job); + await this._saveJob(job); return { id: jobId, success: true }; } catch ({ body }) { return { id: jobId, success: false, error: body }; @@ -738,18 +785,18 @@ export class DataRecognizer { return resp; } - async saveJob(job: ModuleJob) { + private async _saveJob(job: ModuleJob) { return this._mlClient.putJob({ job_id: job.id, body: job.config }); } // save the datafeeds. // if any fail (e.g. it already exists), catch the error and mark the result // as success: false - async saveDatafeeds(datafeeds: ModuleDatafeed[]) { + private async _saveDatafeeds(datafeeds: ModuleDatafeed[]) { return await Promise.all( datafeeds.map(async (datafeed) => { try { - await this.saveDatafeed(datafeed); + await this._saveDatafeed(datafeed); return { id: datafeed.id, success: true, @@ -769,7 +816,7 @@ export class DataRecognizer { ); } - async saveDatafeed(datafeed: ModuleDatafeed) { + private async _saveDatafeed(datafeed: ModuleDatafeed) { return this._mlClient.putDatafeed( { datafeed_id: datafeed.id, @@ -779,19 +826,19 @@ export class DataRecognizer { ); } - async startDatafeeds( + private async _startDatafeeds( datafeeds: ModuleDatafeed[], start?: number, end?: number ): Promise<{ [key: string]: DatafeedResponse }> { const results = {} as { [key: string]: DatafeedResponse }; for (const datafeed of datafeeds) { - results[datafeed.id] = await this.startDatafeed(datafeed, start, end); + results[datafeed.id] = await this._startDatafeed(datafeed, start, end); } return results; } - async startDatafeed( + private async _startDatafeed( datafeed: ModuleDatafeed, start: number | undefined, end: number | undefined @@ -845,7 +892,7 @@ export class DataRecognizer { // merge all of the save results into one result object // which is returned from the endpoint - async updateResults(results: DataRecognizerConfigResponse, saveResults: SaveResults) { + private async _updateResults(results: DataRecognizerConfigResponse, saveResults: SaveResults) { // update job results results.jobs.forEach((j) => { saveResults.jobs.forEach((j2) => { @@ -894,7 +941,7 @@ export class DataRecognizer { // creates an empty results object, // listing each job/datafeed/savedObject with a save success boolean - createResultsTemplate(moduleConfig: Module): DataRecognizerConfigResponse { + private _createResultsTemplate(moduleConfig: Module): DataRecognizerConfigResponse { const results: DataRecognizerConfigResponse = {} as DataRecognizerConfigResponse; const reducedConfig = { jobs: moduleConfig.jobs, @@ -932,7 +979,7 @@ export class DataRecognizer { // if an override index pattern has been specified, // update all of the datafeeds. - updateDatafeedIndices(moduleConfig: Module) { + private _updateDatafeedIndices(moduleConfig: Module) { // if the supplied index pattern contains a comma, split into multiple indices and // add each one to the datafeed const indexPatternNames = splitIndexPatternNames(this._indexPatternName); @@ -962,7 +1009,7 @@ export class DataRecognizer { // loop through the custom urls in each job and replace the INDEX_PATTERN_ID // marker for the id of the specified index pattern - updateJobUrlIndexPatterns(moduleConfig: Module) { + private _updateJobUrlIndexPatterns(moduleConfig: Module) { if (Array.isArray(moduleConfig.jobs)) { moduleConfig.jobs.forEach((job) => { // if the job has custom_urls @@ -986,7 +1033,7 @@ export class DataRecognizer { // check the custom urls in the module's jobs to see if they contain INDEX_PATTERN_ID // which needs replacement - doJobUrlsContainIndexPatternId(moduleConfig: Module) { + private _doJobUrlsContainIndexPatternId(moduleConfig: Module) { if (Array.isArray(moduleConfig.jobs)) { for (const job of moduleConfig.jobs) { // if the job has custom_urls @@ -1004,7 +1051,7 @@ export class DataRecognizer { // loop through each kibana saved object and replace any INDEX_PATTERN_ID and // INDEX_PATTERN_NAME markers for the id or name of the specified index pattern - updateSavedObjectIndexPatterns(moduleConfig: Module) { + private _updateSavedObjectIndexPatterns(moduleConfig: Module) { if (moduleConfig.kibana) { Object.keys(moduleConfig.kibana).forEach((category) => { moduleConfig.kibana[category]!.forEach((item) => { @@ -1037,7 +1084,7 @@ export class DataRecognizer { /** * Provides a time range of the last 3 months of data */ - async getFallbackTimeRange( + private async _getFallbackTimeRange( timeField: string, query?: any ): Promise<{ start: number; end: number }> { @@ -1059,7 +1106,7 @@ export class DataRecognizer { * Ensure the model memory limit for each job is not greater than * the max model memory setting for the cluster */ - async updateModelMemoryLimits( + private async _updateModelMemoryLimits( moduleConfig: Module, estimateMML: boolean, start?: number, @@ -1069,12 +1116,12 @@ export class DataRecognizer { return; } - if (estimateMML && this.jobsForModelMemoryEstimation.length > 0) { + if (estimateMML && this._jobsForModelMemoryEstimation.length > 0) { try { // Checks if all jobs in the module have the same time field configured - const firstJobTimeField = this.jobsForModelMemoryEstimation[0].job.config.data_description + const firstJobTimeField = this._jobsForModelMemoryEstimation[0].job.config.data_description .time_field; - const isSameTimeFields = this.jobsForModelMemoryEstimation.every( + const isSameTimeFields = this._jobsForModelMemoryEstimation.every( ({ job }) => job.config.data_description.time_field === firstJobTimeField ); @@ -1085,16 +1132,16 @@ export class DataRecognizer { const { start: fallbackStart, end: fallbackEnd, - } = await this.getFallbackTimeRange(firstJobTimeField, { match_all: {} }); + } = await this._getFallbackTimeRange(firstJobTimeField, { match_all: {} }); start = fallbackStart; end = fallbackEnd; } - for (const { job, query } of this.jobsForModelMemoryEstimation) { + for (const { job, query } of this._jobsForModelMemoryEstimation) { let earliestMs = start; let latestMs = end; if (earliestMs === undefined || latestMs === undefined) { - const timeFieldRange = await this.getFallbackTimeRange( + const timeFieldRange = await this._getFallbackTimeRange( job.config.data_description.time_field, query ); @@ -1157,7 +1204,7 @@ export class DataRecognizer { // check the kibana saved searches JSON in the module to see if they contain INDEX_PATTERN_ID // which needs replacement - doSavedObjectsContainIndexPatternId(moduleConfig: Module) { + private _doSavedObjectsContainIndexPatternId(moduleConfig: Module) { if (moduleConfig.kibana) { for (const category of Object.keys(moduleConfig.kibana)) { for (const item of moduleConfig.kibana[category]!) { @@ -1171,7 +1218,7 @@ export class DataRecognizer { return false; } - applyJobConfigOverrides( + public applyJobConfigOverrides( moduleConfig: Module, jobOverrides?: JobOverride | JobOverride[], jobPrefix = '' @@ -1205,9 +1252,9 @@ export class DataRecognizer { }); if (generalOverrides.some((override) => !!override.analysis_limits?.model_memory_limit)) { - this.jobsForModelMemoryEstimation = []; + this._jobsForModelMemoryEstimation = []; } else { - this.jobsForModelMemoryEstimation = moduleConfig.jobs + this._jobsForModelMemoryEstimation = moduleConfig.jobs .filter((job) => { const override = jobSpecificOverrides.find((o) => `${jobPrefix}${o.job_id}` === job.id); return override?.analysis_limits?.model_memory_limit === undefined; @@ -1266,7 +1313,7 @@ export class DataRecognizer { }); } - applyDatafeedConfigOverrides( + public applyDatafeedConfigOverrides( moduleConfig: Module, datafeedOverrides?: DatafeedOverride | DatafeedOverride[], jobPrefix = '' diff --git a/x-pack/plugins/ml/server/saved_objects/mappings.json b/x-pack/plugins/ml/server/saved_objects/mappings.json deleted file mode 100644 index 9a23dba324dbf..0000000000000 --- a/x-pack/plugins/ml/server/saved_objects/mappings.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "job": { - "properties": { - "job_id": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "datafeed_id": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword" - } - } - }, - "type": { - "type": "keyword" - } - } - } -} diff --git a/x-pack/plugins/ml/server/saved_objects/mappings.ts b/x-pack/plugins/ml/server/saved_objects/mappings.ts new file mode 100644 index 0000000000000..f452991015723 --- /dev/null +++ b/x-pack/plugins/ml/server/saved_objects/mappings.ts @@ -0,0 +1,90 @@ +/* + * 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 { SavedObjectsTypeMappingDefinition } from 'kibana/server'; + +export const mlJob: SavedObjectsTypeMappingDefinition = { + properties: { + job_id: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + datafeed_id: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + type: { + type: 'keyword', + }, + }, +}; + +export const mlModule: SavedObjectsTypeMappingDefinition = { + dynamic: false, + properties: { + id: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + title: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + description: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + type: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + logo: { + type: 'object', + }, + defaultIndexPattern: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + query: { + type: 'object', + }, + jobs: { + type: 'object', + }, + datafeeds: { + type: 'object', + }, + }, +}; diff --git a/x-pack/plugins/ml/server/saved_objects/saved_objects.ts b/x-pack/plugins/ml/server/saved_objects/saved_objects.ts index e30ff60960e27..004b5e8e554cc 100644 --- a/x-pack/plugins/ml/server/saved_objects/saved_objects.ts +++ b/x-pack/plugins/ml/server/saved_objects/saved_objects.ts @@ -6,10 +6,13 @@ */ import { SavedObjectsServiceSetup } from 'kibana/server'; -import mappings from './mappings.json'; +import { mlJob, mlModule } from './mappings'; import { migrations } from './migrations'; -import { ML_SAVED_OBJECT_TYPE } from '../../common/types/saved_objects'; +import { + ML_SAVED_OBJECT_TYPE, + ML_MODULE_SAVED_OBJECT_TYPE, +} from '../../common/types/saved_objects'; export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup) { savedObjects.registerType({ @@ -17,6 +20,13 @@ export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup) { hidden: false, namespaceType: 'multiple', migrations, - mappings: mappings.job, + mappings: mlJob, + }); + savedObjects.registerType({ + name: ML_MODULE_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + migrations, + mappings: mlModule, }); }