diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 38a46a3cde5a0..8f445ff25218b 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -7,10 +7,10 @@ By default, the Monitoring application is enabled, but data collection is disabled. When you first start {kib} monitoring, you are prompted to -enable data collection. If you are using {security}, you must be +enable data collection. If you are using {security}, you must be signed in as a user with the `cluster:manage` privilege to enable data collection. The built-in `superuser` role has this privilege and the -built-in `elastic` user has this role. +built-in `elastic` user has this role. You can adjust how monitoring data is collected from {kib} and displayed in {kib} by configuring settings in the @@ -134,3 +134,11 @@ For {es} clusters that are running in containers, this setting changes the statistics. It also adds the calculated Cgroup CPU utilization to the *Node Overview* page instead of the overall operating system's CPU utilization. Defaults to `false`. + +`xpack.monitoring.ui.container.logstash.enabled`:: + +For {ls} nodes that are running in containers, this setting +changes the {ls} *Node Listing* to display the CPU utilization +based on the reported Cgroup statistics. It also adds the +calculated Cgroup CPU utilization to the {ls} node detail +pages instead of the overall operating system’s CPU utilization. Defaults to `false`. diff --git a/package.json b/package.json index a623b656ec9a1..365f597fe04fe 100644 --- a/package.json +++ b/package.json @@ -117,8 +117,8 @@ "@elastic/apm-rum": "^4.6.0", "@elastic/charts": "^16.1.0", "@elastic/datemath": "5.0.2", - "@elastic/ems-client": "1.0.5", - "@elastic/eui": "18.0.0", + "@elastic/ems-client": "7.6.0", + "@elastic/eui": "18.2.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "2.3.3", diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index c9434f3ec1c38..af44991e625a2 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "devDependencies": { - "@elastic/eui": "18.0.0", + "@elastic/eui": "18.2.0", "@elastic/charts": "^16.1.0", "@kbn/dev-utils": "1.0.0", "@yarnpkg/lockfile": "^1.1.0", diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 36fe95e05cb53..c63c9384da9d8 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -91,12 +91,25 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { return settings; }; +const mapManifestServiceUrlDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'map.manifestServiceUrl')) { + log( + 'You should no longer use the map.manifestServiceUrl setting in kibana.yml to configure the location ' + + 'of the Elastic Maps Service settings. These settings have moved to the "map.emsTileApiUrl" and ' + + '"map.emsFileApiUrl" settings instead. These settings are for development use only and should not be ' + + 'modified for use in production environments.' + ); + } + return settings; +}; + export const coreDeprecationProvider: ConfigDeprecationProvider = ({ unusedFromRoot, renameFromRoot, }) => [ unusedFromRoot('savedObjects.indexCheckTimeout'), unusedFromRoot('server.xsrf.token'), + unusedFromRoot('maps.manifestServiceUrl'), renameFromRoot('optimize.lazy', 'optimize.watch'), renameFromRoot('optimize.lazyPort', 'optimize.watchPort'), renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), @@ -110,4 +123,5 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ dataPathDeprecation, rewriteBasePathDeprecation, cspRulesDeprecation, + mapManifestServiceUrlDeprecation, ]; diff --git a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js index 2f8a2264530d5..94263e7b76a97 100644 --- a/src/legacy/core_plugins/tests_bundle/tests_entry_template.js +++ b/src/legacy/core_plugins/tests_bundle/tests_entry_template.js @@ -114,7 +114,8 @@ const coreSystem = new CoreSystem({ }, mapConfig: { includeElasticMapsService: true, - manifestServiceUrl: 'https://catalogue-staging.maps.elastic.co/v2/manifest' + emsFileApiUrl: 'https://vector-staging.maps.elastic.co', + emsTileApiUrl: 'https://tiles.maps.elastic.co', }, vegaConfig: { enabled: true, diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson index ebdc5a07af06d..fd7eb1ae7d878 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson +++ b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson @@ -39,7 +39,7 @@ range: { category: {scheme: "elastic"} } - mark: {color: "#00B3A4"} + mark: {color: "#54B399"} } autosize: {type: "fit", contains: "padding"} } diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png index 3f247b57905d4..8f2d146287b08 100644 Binary files a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png and b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png differ diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png index c387c3ec789d3..82077a1096b99 100644 Binary files a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png and b/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png differ diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js index c442f8f17884a..50bcff2469710 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js @@ -53,7 +53,7 @@ describe(`VegaParser._setDefaultColors`, () => { test({}, true, { config: { range: { category: { scheme: 'elastic' } }, - mark: { color: '#5BBAA0' }, + mark: { color: '#54B399' }, }, }) ); @@ -63,15 +63,15 @@ describe(`VegaParser._setDefaultColors`, () => { test({}, false, { config: { range: { category: { scheme: 'elastic' } }, - arc: { fill: '#5BBAA0' }, - area: { fill: '#5BBAA0' }, - line: { stroke: '#5BBAA0' }, - path: { stroke: '#5BBAA0' }, - rect: { fill: '#5BBAA0' }, - rule: { stroke: '#5BBAA0' }, - shape: { stroke: '#5BBAA0' }, - symbol: { fill: '#5BBAA0' }, - trail: { fill: '#5BBAA0' }, + arc: { fill: '#54B399' }, + area: { fill: '#54B399' }, + line: { stroke: '#54B399' }, + path: { stroke: '#54B399' }, + rect: { fill: '#54B399' }, + rule: { stroke: '#54B399' }, + shape: { stroke: '#54B399' }, + symbol: { fill: '#54B399' }, + trail: { fill: '#54B399' }, }, }) ); diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js index 452397877a003..7c2638d1f5165 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js @@ -577,7 +577,7 @@ export class VegaParser { this._setDefaultValue({ scheme: 'elastic' }, 'config', 'range', 'category'); if (this.isVegaLite) { - // Vega-Lite: set default color, works for fill and strike -- config: { mark: { color: '#00B3A4' }} + // Vega-Lite: set default color, works for fill and strike -- config: { mark: { color: '#54B399' }} this._setDefaultValue(defaultColor, 'config', 'mark', 'color'); } else { // Vega - global mark has very strange behavior, must customize each mark type individually diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index a53e8e0498c42..88a794445870c 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -254,7 +254,11 @@ export default () => ) .default([]), }).default(), - manifestServiceUrl: Joi.string().default('https://catalogue.maps.elastic.co/v7.2/manifest'), + manifestServiceUrl: Joi.string() + .default('') + .allow(''), + emsFileApiUrl: Joi.string().default('https://vector-staging.maps.elastic.co'), + emsTileApiUrl: Joi.string().default('https://tiles.maps.elastic.co'), emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.4'), emsFontLibraryUrl: Joi.string().default( 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf' diff --git a/src/legacy/ui/public/styles/bootstrap/_custom_variables.less b/src/legacy/ui/public/styles/bootstrap/_custom_variables.less index aa174684a622b..a348e7bfa86b8 100644 --- a/src/legacy/ui/public/styles/bootstrap/_custom_variables.less +++ b/src/legacy/ui/public/styles/bootstrap/_custom_variables.less @@ -345,7 +345,7 @@ //** Background color of the whole progress component @progress-bg: shade(@gray-lighter, 13%); //** Default progress bar color -@progress-bar-bg: #00B3A4; +@progress-bar-bg: #54B399; //== List group // diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files.json index 3ef17ea35352c..cdbed7fa06367 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files.json +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_files.json @@ -24,7 +24,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/world_countries_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/world_countries_v1.geo.json", "legacy_default": true } ], @@ -430,7 +430,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/australia_states_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/australia_states_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -629,7 +629,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/canada_provinces_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/canada_provinces_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -908,7 +908,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/china_provinces_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/china_provinces_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -1266,7 +1266,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/finland_regions_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/finland_regions_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -1634,7 +1634,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/france_departments_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/france_departments_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -1984,7 +1984,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/germany_states_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/germany_states_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -2328,7 +2328,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/ireland_counties_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/ireland_counties_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -2637,7 +2637,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/japan_prefectures_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/japan_prefectures_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -3003,7 +3003,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/netherlands_provinces_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/netherlands_provinces_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -3309,7 +3309,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/norway_counties_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/norway_counties_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -3671,7 +3671,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/spain_provinces_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/spain_provinces_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -4002,7 +4002,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/sweden_counties_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/sweden_counties_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -4311,7 +4311,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/switzerland_cantons_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/switzerland_cantons_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -4827,7 +4827,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/uk_subdivisions_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/uk_subdivisions_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -5074,7 +5074,7 @@ "formats": [ { "type": "topojson", - "url": "https://vector-staging.maps.elastic.co/files/usa_counties_v2.topo.json?elastic_tile_service_tos=agree", + "url": "/files/usa_counties_v2.topo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -5441,7 +5441,7 @@ "formats": [ { "type": "geojson", - "url": "https://vector-staging.maps.elastic.co/files/usa_states_v1.geo.json?elastic_tile_service_tos=agree", + "url": "/files/usa_states_v1.geo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], @@ -5731,7 +5731,7 @@ "formats": [ { "type": "topojson", - "url": "https://vector-staging.maps.elastic.co/files/usa_zip_codes_v2.topo.json?elastic_tile_service_tos=agree", + "url": "/files/usa_zip_codes_v2.topo.json?elastic_tile_service_tos=agree", "legacy_default": true } ], diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json index aaf1edbf4860e..6030c8068884d 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_manifest.json @@ -3,13 +3,13 @@ { "id": "tiles_v2", "name": "Elastic Maps Tile Service", - "manifest": "https://tiles.foobar/manifest", + "manifest": "https://tiles.foobar/v7.6/manifest", "type": "tms" }, { "id": "geo_layers", "name": "Elastic Maps Vector Service", - "manifest": "https://files.foobar/manifest", + "manifest": "https://files.foobar/v7.6/manifest", "type": "file" } ] diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright.json index 6ea1686dadb8d..f757624ffbca7 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright.json +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright.json @@ -7,6 +7,6 @@ "bounds": [-180, -85.0511, 180, 85.0511], "format": "png", "type": "baselayer", - "tiles": ["https://raster-style.foobar/styles/osm-bright/{z}/{x}/{y}.png"], + "tiles": ["/raster/styles/osm-bright/{z}/{x}/{y}.png"], "center": [0, 0, 2] } diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector.json index b14db52644459..52b70bff6b2ad 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector.json +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector.json @@ -41,11 +41,11 @@ "sources": { "openmaptiles": { "type": "vector", - "url": "https://tiles.maps.elastic.co/data/v3.json" + "url": "/data/v3.json" } }, - "sprite": "https://tiles.maps.elastic.co/styles/osm-bright/sprite", - "glyphs": "https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf", + "sprite": "/styles/osm-bright/sprite", + "glyphs": "/fonts/{fontstack}/{range}.pbf", "layers": [ { "id": "background", diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector_source.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector_source.json index a32b627dba2c2..9961d54028b13 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector_source.json +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_bright_vector_source.json @@ -1,6 +1,6 @@ { "tiles": [ - "https://tiles.maps.elastic.co/data/v3/{z}/{x}/{y}.pbf" + "/data/v3/{z}/{x}/{y}.pbf" ], "name": "OpenMapTiles", "format": "pbf", diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_dark.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_dark.json index 9481297b99a28..411d9d59b89c6 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_dark.json +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_dark.json @@ -7,6 +7,6 @@ "bounds": [-180, -85.0511, 180, 85.0511], "format": "png", "type": "baselayer", - "tiles": ["https://raster-style.foobar/styles/dark-matter/{z}/{x}/{y}.png"], + "tiles": ["/raster/styles/dark-matter/{z}/{x}/{y}.png"], "center": [0, 0, 2] } diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated.json index cbbd35d59ce89..c89bbe73b603a 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated.json +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_style_desaturated.json @@ -7,6 +7,6 @@ "bounds": [-180, -85.0511, 180, 85.0511], "format": "png", "type": "baselayer", - "tiles": ["https://raster-style.foobar/styles/osm-bright-desaturated/{z}/{x}/{y}.png"], + "tiles": ["/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png"], "center": [0, 0, 2] } diff --git a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json index 9df72817bb940..c038bb411daec 100644 --- a/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json +++ b/src/legacy/ui/public/vis/__tests__/map/ems_mocks/sample_tiles.json @@ -19,12 +19,12 @@ { "locale": "en", "format": "vector", - "url": "https://vector-style.foobar/styles/osm-bright/style.json" + "url": "/v7.6/styles/osm-bright/style.json" }, { "locale": "en", "format": "raster", - "url": "https://raster-style.foobar/styles/osm-bright.json" + "url": "/v7.6/styles/osm-bright.json" } ] }, @@ -47,12 +47,12 @@ { "locale": "en", "format": "vector", - "url": "https://vector-style.foobar/styles/osm-bright-desaturated/style.json" + "url": "/v7.6/styles/osm-bright-desaturated/style.json" }, { "locale": "en", "format": "raster", - "url": "https://raster-style.foobar/styles/osm-bright-desaturated.json" + "url": "/v7.6/styles/osm-bright-desaturated.json" } ] }, @@ -75,12 +75,12 @@ { "locale": "en", "format": "vector", - "url": "https://vector-style.foobar/styles/dark-matter/style.json" + "url": "/v7.6/styles/dark-matter/style.json" }, { "locale": "en", "format": "raster", - "url": "https://raster-style.foobar/styles/dark-matter.json" + "url": "/v7.6/styles/dark-matter.json" } ] } diff --git a/src/legacy/ui/public/vis/__tests__/map/service_settings.js b/src/legacy/ui/public/vis/__tests__/map/service_settings.js index 820b66897affa..61925760457c6 100644 --- a/src/legacy/ui/public/vis/__tests__/map/service_settings.js +++ b/src/legacy/ui/public/vis/__tests__/map/service_settings.js @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import url from 'url'; -import EMS_CATALOGUE from './ems_mocks/sample_manifest.json'; import EMS_FILES from './ems_mocks/sample_files.json'; import EMS_TILES from './ems_mocks/sample_tiles.json'; import EMS_STYLE_ROAD_MAP_BRIGHT from './ems_mocks/sample_style_bright'; @@ -34,14 +33,18 @@ describe('service_settings (FKA tilemaptest)', function() { let mapConfig; let tilemapsConfig; - const manifestUrl = 'https://foobar/manifest'; - const manifestUrl2 = 'https://foobar_override/v1/manifest'; + const emsFileApiUrl = 'https://files.foobar'; + const emsTileApiUrl = 'https://tiles.foobar'; + + const emsTileApiUrl2 = 'https://tiles_override.foobar'; + const emsFileApiUrl2 = 'https://files_override.foobar'; beforeEach( ngMock.module('kibana', $provide => { $provide.decorator('mapConfig', () => { return { - manifestServiceUrl: manifestUrl, + emsFileApiUrl, + emsTileApiUrl, includeElasticMapsService: true, emsTileLayerId: { bright: 'road_map', @@ -53,7 +56,8 @@ describe('service_settings (FKA tilemaptest)', function() { }) ); - let manifestServiceUrlOriginal; + let emsTileApiUrlOriginal; + let emsFileApiUrlOriginal; let tilemapsConfigDeprecatedOriginal; let getManifestStub; beforeEach( @@ -61,26 +65,26 @@ describe('service_settings (FKA tilemaptest)', function() { serviceSettings = $injector.get('serviceSettings'); getManifestStub = serviceSettings.__debugStubManifestCalls(async url => { //simulate network calls - if (url.startsWith('https://foobar')) { - return EMS_CATALOGUE; - } else if (url.startsWith('https://tiles.foobar')) { - return EMS_TILES; - } else if (url.startsWith('https://files.foobar')) { - return EMS_FILES; - } else if (url.startsWith('https://raster-style.foobar')) { - if (url.includes('osm-bright-desaturated')) { + if (url.startsWith('https://tiles.foobar')) { + if (url.includes('/manifest')) { + return EMS_TILES; + } else if (url.includes('osm-bright-desaturated.json')) { return EMS_STYLE_ROAD_MAP_DESATURATED; - } else if (url.includes('osm-bright')) { + } else if (url.includes('osm-bright.json')) { return EMS_STYLE_ROAD_MAP_BRIGHT; - } else if (url.includes('dark-matter')) { + } else if (url.includes('dark-matter.json')) { return EMS_STYLE_DARK_MAP; } + } else if (url.startsWith('https://files.foobar')) { + return EMS_FILES; } }); mapConfig = $injector.get('mapConfig'); tilemapsConfig = $injector.get('tilemapsConfig'); - manifestServiceUrlOriginal = mapConfig.manifestServiceUrl; + emsTileApiUrlOriginal = mapConfig.emsTileApiUrl; + emsFileApiUrlOriginal = mapConfig.emsFileApiUrl; + tilemapsConfigDeprecatedOriginal = tilemapsConfig.deprecated; $rootScope.$digest(); }) @@ -88,7 +92,8 @@ describe('service_settings (FKA tilemaptest)', function() { afterEach(function() { getManifestStub.removeStub(); - mapConfig.manifestServiceUrl = manifestServiceUrlOriginal; + mapConfig.emsTileApiUrl = emsTileApiUrlOriginal; + mapConfig.emsFileApiUrl = emsFileApiUrlOriginal; tilemapsConfig.deprecated = tilemapsConfigDeprecatedOriginal; }); @@ -110,7 +115,7 @@ describe('service_settings (FKA tilemaptest)', function() { expect(attrs.url).to.contain('{z}'); const urlObject = url.parse(attrs.url, true); - expect(urlObject.hostname).to.be('raster-style.foobar'); + expect(urlObject.hostname).to.be('tiles.foobar'); expect(urlObject.query).to.have.property('my_app_name', 'kibana'); expect(urlObject.query).to.have.property('elastic_tile_service_tos', 'agree'); expect(urlObject.query).to.have.property('my_app_version'); @@ -161,7 +166,8 @@ describe('service_settings (FKA tilemaptest)', function() { }); it('when overridden, should continue to work', async () => { - mapConfig.manifestServiceUrl = manifestUrl2; + mapConfig.emsFileApiUrl = emsFileApiUrl2; + mapConfig.emsTileApiUrl = emsTileApiUrl2; serviceSettings.addQueryParams({ foo: 'bar' }); tilemapServices = await serviceSettings.getTMSServices(); await assertQuery({ foo: 'bar' }); @@ -187,11 +193,11 @@ describe('service_settings (FKA tilemaptest)', function() { id: 'road_map', name: 'Road Map - Bright', url: - 'https://raster-style.foobar/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3', + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3', minZoom: 0, maxZoom: 10, attribution: - '

OpenStreetMap contributors | OpenMapTiles | MapTiler | Elastic Maps Service

', + 'OpenStreetMap contributors | OpenMapTiles | MapTiler | Elastic Maps Service', subdomains: [], }, ]; @@ -233,19 +239,19 @@ describe('service_settings (FKA tilemaptest)', function() { ); expect(desaturationFalse.url).to.equal( - 'https://raster-style.foobar/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' ); expect(desaturationFalse.maxZoom).to.equal(10); expect(desaturationTrue.url).to.equal( - 'https://raster-style.foobar/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/osm-bright-desaturated/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' ); expect(desaturationTrue.maxZoom).to.equal(18); expect(darkThemeDesaturationFalse.url).to.equal( - 'https://raster-style.foobar/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' ); expect(darkThemeDesaturationFalse.maxZoom).to.equal(22); expect(darkThemeDesaturationTrue.url).to.equal( - 'https://raster-style.foobar/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' + 'https://tiles.foobar/raster/styles/dark-matter/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana&my_app_version=1.2.3' ); expect(darkThemeDesaturationTrue.maxZoom).to.equal(22); }); diff --git a/src/legacy/ui/public/vis/editors/default/controls/precision.tsx b/src/legacy/ui/public/vis/editors/default/controls/precision.tsx index 88f389cb7c009..4fe9eede8465e 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/precision.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/precision.tsx @@ -40,7 +40,7 @@ function PrecisionParamEditor({ agg, value, setValue }: AggParamEditorProps | React.MouseEvent) => setValue(Number(ev.currentTarget.value)) } diff --git a/src/legacy/ui/public/vis/map/service_settings.js b/src/legacy/ui/public/vis/map/service_settings.js index 1096aa8eb4503..233ee526c439b 100644 --- a/src/legacy/ui/public/vis/map/service_settings.js +++ b/src/legacy/ui/public/vis/map/service_settings.js @@ -48,7 +48,8 @@ uiModules this._emsClient = new EMSClient({ language: i18n.getLocale(), kbnVersion: kbnVersion, - manifestServiceUrl: mapConfig.manifestServiceUrl, + fileApiUrl: mapConfig.emsFileApiUrl, + tileApiUrl: mapConfig.emsTileApiUrl, htmlSanitizer: $sanitize, landingPageUrl: mapConfig.emsLandingPageUrl, }); diff --git a/src/plugins/es_ui_shared/public/components/json_editor/index.ts b/src/plugins/es_ui_shared/public/components/json_editor/index.ts new file mode 100644 index 0000000000000..81476a65f4215 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/json_editor/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './json_editor'; + +export { OnJsonEditorUpdateHandler } from './use_json'; diff --git a/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx b/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx new file mode 100644 index 0000000000000..8c63cc8494a8b --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/json_editor/json_editor.tsx @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback } from 'react'; +import { EuiFormRow, EuiCodeEditor } from '@elastic/eui'; +import { debounce } from 'lodash'; + +import { isJSON } from '../../../static/validators/string'; +import { useJson, OnJsonEditorUpdateHandler } from './use_json'; + +interface Props { + onUpdate: OnJsonEditorUpdateHandler; + label?: string; + helpText?: React.ReactNode; + value?: string; + defaultValue?: { [key: string]: any }; + euiCodeEditorProps?: { [key: string]: any }; + error?: string | null; +} + +export const JsonEditor = React.memo( + ({ + label, + helpText, + onUpdate, + value, + defaultValue, + euiCodeEditorProps, + error: propsError, + }: Props) => { + const isControlled = value !== undefined; + + const { content, setContent, error: internalError } = useJson({ + defaultValue, + onUpdate, + isControlled, + }); + + const debouncedSetContent = useCallback(debounce(setContent, 300), [setContent]); + + // We let the consumer control the validation and the error message. + const error = isControlled ? propsError : internalError; + + const onEuiCodeEditorChange = useCallback( + (updated: string) => { + if (isControlled) { + onUpdate({ + data: { + raw: updated, + format() { + return JSON.parse(updated); + }, + }, + validate() { + return isJSON(updated); + }, + isValid: undefined, + }); + } else { + debouncedSetContent(updated); + } + }, + [isControlled] + ); + + return ( + + + + ); + } +); diff --git a/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts b/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts new file mode 100644 index 0000000000000..1b5ca5d7f4384 --- /dev/null +++ b/src/plugins/es_ui_shared/public/components/json_editor/use_json.ts @@ -0,0 +1,94 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useEffect, useState, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { isJSON } from '../../../static/validators/string'; + +export type OnJsonEditorUpdateHandler = (arg: { + data: { + raw: string; + format(): T; + }; + validate(): boolean; + isValid: boolean | undefined; +}) => void; + +interface Parameters { + onUpdate: OnJsonEditorUpdateHandler; + defaultValue?: T; + isControlled?: boolean; +} + +const stringifyJson = (json: { [key: string]: any }) => + Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}'; + +export const useJson = ({ + defaultValue = {} as T, + onUpdate, + isControlled = false, +}: Parameters) => { + const didMount = useRef(false); + const [content, setContent] = useState(stringifyJson(defaultValue)); + const [error, setError] = useState(null); + + const validate = () => { + // We allow empty string as it will be converted to "{}"" + const isValid = content.trim() === '' ? true : isJSON(content); + if (!isValid) { + setError( + i18n.translate('esUi.validation.string.invalidJSONError', { + defaultMessage: 'Invalid JSON', + }) + ); + } else { + setError(null); + } + return isValid; + }; + + const formatContent = () => { + const isValid = validate(); + const data = isValid && content.trim() !== '' ? JSON.parse(content) : {}; + return data as T; + }; + + useEffect(() => { + if (didMount.current) { + const isValid = isControlled ? undefined : validate(); + onUpdate({ + data: { + raw: content, + format: formatContent, + }, + validate, + isValid, + }); + } else { + didMount.current = true; + } + }, [content]); + + return { + content, + setContent, + error, + }; +}; diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts new file mode 100644 index 0000000000000..a12c951ad13a8 --- /dev/null +++ b/src/plugins/es_ui_shared/public/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './components/json_editor'; diff --git a/src/plugins/es_ui_shared/static/forms/components/field.tsx b/src/plugins/es_ui_shared/static/forms/components/field.tsx index 5b9a6dc9de002..07fca1a7f7595 100644 --- a/src/plugins/es_ui_shared/static/forms/components/field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/field.tsx @@ -17,12 +17,12 @@ * under the License. */ -import React from 'react'; +import React, { ComponentType } from 'react'; import { FieldHook, FIELD_TYPES } from '../hook_form_lib'; interface Props { field: FieldHook; - euiFieldProps?: Record; + euiFieldProps?: { [key: string]: any }; idAria?: string; [key: string]: any; } @@ -41,7 +41,7 @@ import { ToggleField, } from './fields'; -const mapTypeToFieldComponent = { +const mapTypeToFieldComponent: { [key: string]: ComponentType } = { [FIELD_TYPES.TEXT]: TextField, [FIELD_TYPES.TEXTAREA]: TextAreaField, [FIELD_TYPES.NUMBER]: NumericField, diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx index 0443b4ff09e60..c8ba9f5ac4102 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/checkbox_field.tsx @@ -35,7 +35,7 @@ export const CheckBoxField = ({ field, euiFieldProps = {}, ...rest }: Props) => return ( { + const { errorMessage } = getFieldValidityAndErrorMessage(field); + + const { label, helpText, value, setValue } = field; + + const onJsonUpdate: OnJsonEditorUpdateHandler = useCallback( + updatedJson => { + setValue(updatedJson.data.raw); + }, + [setValue] + ); + + return ( + + ); +}; diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/multi_select_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/multi_select_field.tsx index 0f7332e3954e6..e77337e4ecf53 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/multi_select_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/multi_select_field.tsx @@ -35,7 +35,7 @@ export const MultiSelectField = ({ field, euiFieldProps = {}, ...rest }: Props) return ( { return ( { return ( ; + euiFieldProps: { + options: Array< + { text: string | ReactNode; [key: string]: any } & OptionHTMLAttributes + >; + [key: string]: any; + }; idAria?: string; [key: string]: any; } -export const SelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => { +export const SelectField = ({ field, euiFieldProps, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( ; + euiFieldProps: { + options: EuiSuperSelectProps['options']; + [key: string]: any; + }; idAria?: string; [key: string]: any; } -export const SuperSelectField = ({ field, euiFieldProps = {}, ...rest }: Props) => { +export const SuperSelectField = ({ field, euiFieldProps = { options: [] }, ...rest }: Props) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( { field.setValue(value); }} - options={[]} isInvalid={isInvalid} data-test-subj="select" {...euiFieldProps} diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx index b9c6424a00656..c6fccb0c0e383 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/text_area_field.tsx @@ -35,7 +35,7 @@ export const TextAreaField = ({ field, euiFieldProps = {}, ...rest }: Props) => return ( { return ( { return ( ( + ...args: Parameters +): ReturnType> => { + const [{ value }] = args; + + if (typeof value !== 'string') { + return; + } + + if (!isJSON(value)) { + return { + code: 'ERR_JSON_FORMAT', + message, + }; + } +}; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/lowercase_string.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/lowercase_string.ts new file mode 100644 index 0000000000000..42de66b930eaa --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/lowercase_string.ts @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ValidationFunc } from '../../hook_form_lib'; +import { isLowerCaseString } from '../../../validators/string'; +import { ERROR_CODE } from './types'; + +export const lowerCaseStringField = (message: string) => ( + ...args: Parameters +): ReturnType> => { + const [{ value }] = args; + + if (typeof value !== 'string') { + return; + } + + if (!isLowerCaseString(value)) { + return { + code: 'ERR_LOWERCASE_STRING', + message, + }; + } +}; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_greater_than.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_greater_than.ts new file mode 100644 index 0000000000000..767302a8328c1 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_greater_than.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ValidationFunc, ValidationError } from '../../hook_form_lib'; +import { isNumberGreaterThan } from '../../../validators/number'; +import { ERROR_CODE } from './types'; + +export const numberGreaterThanField = ({ + than, + message, + allowEquality = false, +}: { + than: number; + message: string | ((err: Partial) => string); + allowEquality?: boolean; +}) => (...args: Parameters): ReturnType> => { + const [{ value }] = args; + + return isNumberGreaterThan(than, allowEquality)(value as number) + ? undefined + : { + code: 'ERR_GREATER_THAN_NUMBER', + than, + message: typeof message === 'function' ? message({ than }) : message, + }; +}; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_smaller_than.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_smaller_than.ts new file mode 100644 index 0000000000000..4eab3c5b0f103 --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/number_smaller_than.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ValidationFunc, ValidationError } from '../../hook_form_lib'; +import { isNumberSmallerThan } from '../../../validators/number'; +import { ERROR_CODE } from './types'; + +export const numberSmallerThanField = ({ + than, + message, + allowEquality = false, +}: { + than: number; + message: string | ((err: Partial) => string); + allowEquality?: boolean; +}) => (...args: Parameters): ReturnType> => { + const [{ value }] = args; + + return isNumberSmallerThan(than, allowEquality)(value as number) + ? undefined + : { + code: 'ERR_SMALLER_THAN_NUMBER', + than, + message: typeof message === 'function' ? message({ than }) : message, + }; +}; diff --git a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts index 25cf038ec227d..d5bceac836137 100644 --- a/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts +++ b/src/plugins/es_ui_shared/static/forms/helpers/field_validators/types.ts @@ -25,4 +25,8 @@ export type ERROR_CODE = | 'ERR_MIN_LENGTH' | 'ERR_MAX_LENGTH' | 'ERR_MIN_SELECTION' - | 'ERR_MAX_SELECTION'; + | 'ERR_MAX_SELECTION' + | 'ERR_LOWERCASE_STRING' + | 'ERR_JSON_FORMAT' + | 'ERR_SMALLER_THAN_NUMBER' + | 'ERR_GREATER_THAN_NUMBER'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts index 06f5c2369df10..a8d24984cec7c 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider.ts @@ -17,10 +17,9 @@ * under the License. */ -import { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { FormData } from '../types'; -import { Subscription } from '../lib'; import { useFormContext } from '../form_context'; interface Props { @@ -28,14 +27,13 @@ interface Props { pathsToWatch?: string | string[]; } -export const FormDataProvider = ({ children, pathsToWatch }: Props) => { +export const FormDataProvider = React.memo(({ children, pathsToWatch }: Props) => { const [formData, setFormData] = useState({}); - const previousState = useRef({}); - const subscription = useRef(null); + const previousRawData = useRef({}); const form = useFormContext(); useEffect(() => { - subscription.current = form.__formData$.current.subscribe(data => { + const subscription = form.subscribe(({ data: { raw } }) => { // To avoid re-rendering the children for updates on the form data // that we are **not** interested in, we can specify one or multiple path(s) // to watch. @@ -43,19 +41,17 @@ export const FormDataProvider = ({ children, pathsToWatch }: Props) => { const valuesToWatchArray = Array.isArray(pathsToWatch) ? (pathsToWatch as string[]) : ([pathsToWatch] as string[]); - if (valuesToWatchArray.some(value => previousState.current[value] !== data[value])) { - previousState.current = data; - setFormData(data); + if (valuesToWatchArray.some(value => previousRawData.current[value] !== raw[value])) { + previousRawData.current = raw; + setFormData(raw); } } else { - setFormData(data); + setFormData(raw); } }); - return () => { - subscription.current!.unsubscribe(); - }; - }, [pathsToWatch]); + return subscription.unsubscribe; + }, [form, pathsToWatch]); return children(formData); -}; +}); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/index.ts index b5f1d18ee9e64..1088a3f82aa4f 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/index.ts @@ -19,5 +19,6 @@ export * from './form'; export * from './use_field'; +export * from './use_multi_fields'; export * from './use_array'; export * from './form_data_provider'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx index 580ec7714027c..021d52fbe9edb 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -17,80 +17,71 @@ * under the License. */ -import React, { useEffect, FunctionComponent } from 'react'; +import React, { FunctionComponent } from 'react'; import { FieldHook, FieldConfig } from '../types'; import { useField } from '../hooks'; import { useFormContext } from '../form_context'; -interface Props { +export interface Props { path: string; config?: FieldConfig; defaultValue?: unknown; component?: FunctionComponent | 'input'; componentProps?: Record; + onChange?: (value: unknown) => void; children?: (field: FieldHook) => JSX.Element; } -export const UseField = ({ - path, - config, - defaultValue, - component = 'input', - componentProps = {}, - children, -}: Props) => { - const form = useFormContext(); +export const UseField = React.memo( + ({ path, config, defaultValue, component, componentProps, onChange, children }: Props) => { + const form = useFormContext(); + component = component === undefined ? 'input' : component; + componentProps = componentProps === undefined ? {} : componentProps; - if (typeof defaultValue === 'undefined') { - defaultValue = form.getFieldDefaultValue(path); - } + if (typeof defaultValue === 'undefined') { + defaultValue = form.getFieldDefaultValue(path); + } - if (!config) { - config = form.__readFieldConfigFromSchema(path); - } + if (!config) { + config = form.__readFieldConfigFromSchema(path); + } - // Don't modify the config object - const configCopy = - typeof defaultValue !== 'undefined' ? { ...config, defaultValue } : { ...config }; + // Don't modify the config object + const configCopy = + typeof defaultValue !== 'undefined' ? { ...config, defaultValue } : { ...config }; - if (!configCopy.path) { - configCopy.path = path; - } else { - if (configCopy.path !== path) { - throw new Error( - `Field path mismatch. Got "${path}" but field config has "${configCopy.path}".` - ); + if (!configCopy.path) { + configCopy.path = path; + } else { + if (configCopy.path !== path) { + throw new Error( + `Field path mismatch. Got "${path}" but field config has "${configCopy.path}".` + ); + } } - } - const field = useField(form, path, configCopy); + const field = useField(form, path, configCopy, onChange); - // Remove field from form when it is unmounted or if its path changes - useEffect(() => { - return () => { - form.__removeField(path); - }; - }, [path]); + // Children prevails over anything else provided. + if (children) { + return children!(field); + } - // Children prevails over anything else provided. - if (children) { - return children!(field); - } + if (component === 'input') { + return ( + + ); + } - if (component === 'input') { - return ( - - ); + return component({ field, ...componentProps }); } - - return component({ field, ...componentProps }); -}; +); /** * Get a component providing some common props for all instances. diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx new file mode 100644 index 0000000000000..b84c5585e017c --- /dev/null +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +import { UseField, Props as UseFieldProps } from './use_field'; +import { FieldHook } from '../types'; + +type FieldsArray = Array<{ id: string } & Omit>; + +interface Props { + fields: { [key: string]: Omit }; + children: (fields: { [key: string]: FieldHook }) => JSX.Element; +} + +export const UseMultiFields = ({ fields, children }: Props) => { + const fieldsArray = Object.entries(fields).reduce( + (acc, [fieldId, field]) => [...acc, { id: fieldId, ...field }], + [] as FieldsArray + ); + + const hookFields: { [key: string]: FieldHook } = {}; + + const renderField = (index: number) => { + const { id } = fieldsArray[index]; + return ( + + {field => { + hookFields[id] = field; + return index === fieldsArray.length - 1 ? children(hookFields) : renderField(index + 1); + }} + + ); + }; + + if (!Boolean(fieldsArray.length)) { + return null; + } + + return renderField(0); +}; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx index b7c6c39e7b0c5..5dcd076b41533 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_context.tsx @@ -19,7 +19,7 @@ import React, { createContext, useContext } from 'react'; -import { FormHook } from './types'; +import { FormHook, FormData } from './types'; const FormContext = createContext | undefined>(undefined); @@ -32,7 +32,7 @@ export const FormProvider = ({ children, form }: Props) => ( {children} ); -export const useFormContext = function>() { +export const useFormContext = function() { const context = useContext(FormContext) as FormHook; if (context === undefined) { throw new Error('useFormContext must be used within a '); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index d7ef798bf2e03..80cddb513b20a 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -17,12 +17,17 @@ * under the License. */ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; import { FormHook, FieldHook, FieldConfig, FieldValidateResponse, ValidationError } from '../types'; import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; -export const useField = (form: FormHook, path: string, config: FieldConfig = {}) => { +export const useField = ( + form: FormHook, + path: string, + config: FieldConfig = {}, + valueChangeListener?: (value: unknown) => void +) => { const { type = FIELD_TYPES.TEXT, defaultValue = '', @@ -37,17 +42,25 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) deserializer = (value: unknown) => value, } = config; - const [value, setStateValue] = useState( - typeof defaultValue === 'function' ? deserializer(defaultValue()) : deserializer(defaultValue) + const initialValue = useMemo( + () => + typeof defaultValue === 'function' + ? deserializer(defaultValue()) + : deserializer(defaultValue), + [defaultValue] ); + + const [value, setStateValue] = useState(initialValue); const [errors, setErrors] = useState([]); const [isPristine, setPristine] = useState(true); const [isValidating, setValidating] = useState(false); const [isChangingValue, setIsChangingValue] = useState(false); + const [isValidated, setIsValidated] = useState(false); const validateCounter = useRef(0); const changeCounter = useRef(0); const inflightValidation = useRef | null>(null); const debounceTimeout = useRef(null); + const isUnmounted = useRef(false); // -- HELPERS // ---------------------------------- @@ -77,7 +90,10 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) if (isEmptyString) { return inputValue; } - return formatters.reduce((output, formatter) => formatter(output), inputValue); + + const formData = form.getFormData({ unflatten: false }); + + return formatters.reduce((output, formatter) => formatter(output, formData), inputValue); }; const onValueChange = async () => { @@ -92,12 +108,23 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) setIsChangingValue(true); } + const newValue = serializeOutput(value); + + // Notify listener + if (valueChangeListener) { + valueChangeListener(newValue); + } + // Update the form data observable - form.__updateFormDataAt(path, serializeOutput(value)); + form.__updateFormDataAt(path, newValue); // Validate field(s) and set form.isValid flag await form.__validateFields(fieldsToValidateOnChange); + if (isUnmounted.current) { + return; + } + /** * If we have set a delay to display the error message after the field value has changed, * we first check that this is the last "change iteration" (=== the last keystroke from the user) @@ -263,6 +290,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) validationType, } = validationData; + setIsValidated(true); setValidating(true); // By the time our validate function has reached completion, it’s possible @@ -276,12 +304,10 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) // This is the most recent invocation setValidating(false); // Update the errors array - setErrors(previousErrors => { - // First filter out the validation type we are currently validating - const filteredErrors = filterErrors(previousErrors, validationType); - return [...filteredErrors, ..._validationErrors]; - }); + const filteredErrors = filterErrors(errors, validationType); + setErrors([...filteredErrors, ..._validationErrors]); } + return { isValid: _validationErrors.length === 0, errors: _validationErrors, @@ -359,6 +385,22 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) return errorMessages ? errorMessages : null; }; + const reset: FieldHook['reset'] = (resetOptions = { resetValue: true }) => { + const { resetValue = true } = resetOptions; + + setPristine(true); + setValidating(false); + setIsChangingValue(false); + setIsValidated(false); + setErrors([]); + + if (resetValue) { + setValue(initialValue); + return initialValue; + } + return value; + }; + const serializeOutput: FieldHook['__serializeOutput'] = (rawValue = value) => serializer(rawValue); @@ -390,6 +432,7 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) form, isPristine, isValidating, + isValidated, isChangingValue, onChange, getErrorsMessages, @@ -397,10 +440,32 @@ export const useField = (form: FormHook, path: string, config: FieldConfig = {}) setErrors: _setErrors, clearErrors, validate, + reset, __serializeOutput: serializeOutput, }; - form.__addField(field); + form.__addField(field); // Executed first (1) + + useEffect(() => { + /** + * NOTE: effect cleanup actually happens *after* the new component has been mounted, + * but before the next effect callback is run. + * Ref: https://kentcdodds.com/blog/understanding-reacts-key-prop + * + * This means that, the "form.__addField(field)" outside the effect will be called *before* + * the cleanup `form.__removeField(path);` creating a race condition. + * + * TODO: See how we could refactor "use_field" & "use_form" to avoid having the + * `form.__addField(field)` call outside the effect. + */ + form.__addField(field); // Executed third (3) + + return () => { + // Remove field from the form when it is unmounted or if its path changes. + isUnmounted.current = true; + form.__removeField(path); // Executed second (2) + }; + }, [path]); return field; }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 3902b0615a33d..d8b2f35e117a6 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -17,11 +17,11 @@ * under the License. */ -import { useState, useRef } from 'react'; +import { useState, useRef, useEffect, useMemo } from 'react'; import { get } from 'lodash'; -import { FormHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; -import { mapFormFields, flattenObject, unflattenObject, Subject } from '../lib'; +import { FormHook, FieldHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; +import { mapFormFields, flattenObject, unflattenObject, Subject, Subscription } from '../lib'; const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500; const DEFAULT_OPTIONS = { @@ -29,35 +29,54 @@ const DEFAULT_OPTIONS = { stripEmptyFields: true, }; -interface UseFormReturn { +interface UseFormReturn { form: FormHook; } -export function useForm( +export function useForm( formConfig: FormConfig | undefined = {} ): UseFormReturn { const { onSubmit, schema, - defaultValue = {}, serializer = (data: any) => data, deserializer = (data: any) => data, options = {}, } = formConfig; + + const formDefaultValue = + formConfig.defaultValue === undefined || Object.keys(formConfig.defaultValue).length === 0 + ? {} + : Object.entries(formConfig.defaultValue as object) + .filter(({ 1: value }) => value !== undefined) + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + const formOptions = { ...DEFAULT_OPTIONS, ...options }; - const defaultValueDeserialized = - Object.keys(defaultValue).length === 0 ? defaultValue : deserializer(defaultValue); - const [isSubmitted, setSubmitted] = useState(false); + const defaultValueDeserialized = useMemo(() => deserializer(formDefaultValue), [ + formConfig.defaultValue, + ]); + + const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitting, setSubmitting] = useState(false); - const [isValid, setIsValid] = useState(true); + const [isValid, setIsValid] = useState(undefined); const fieldsRefs = useRef({}); + const formUpdateSubscribers = useRef([]); + const isUnmounted = useRef(false); // formData$ is an observable we can subscribe to in order to receive live // update of the raw form data. As an observable it does not trigger any React // render(). // The component is the one in charge of reading this observable // and updating its state to trigger the necessary view render. - const formData$ = useRef>(new Subject(flattenObject(defaultValue) as T)); + const formData$ = useRef>(new Subject(flattenObject(formDefaultValue) as T)); + + useEffect(() => { + return () => { + formUpdateSubscribers.current.forEach(subscription => subscription.unsubscribe()); + formUpdateSubscribers.current = []; + isUnmounted.current = true; + }; + }, []); // -- HELPERS // ---------------------------------- @@ -75,6 +94,12 @@ export function useForm( return fields; }; + const updateFormDataAt: FormHook['__updateFormDataAt'] = (path, value) => { + const currentFormData = formData$.current.value; + formData$.current.next({ ...currentFormData, [path]: value }); + return formData$.current.value; + }; + // -- API // ---------------------------------- const getFormData: FormHook['getFormData'] = (getDataOptions = { unflatten: true }) => @@ -90,43 +115,76 @@ export function useForm( {} as T ); - const updateFormDataAt: FormHook['__updateFormDataAt'] = (path, value) => { - const currentFormData = formData$.current.value; - formData$.current.next({ ...currentFormData, [path]: value }); - return formData$.current.value; + const getErrors: FormHook['getErrors'] = () => { + if (isValid === true) { + return []; + } + + return fieldsToArray().reduce((acc, field) => { + const fieldError = field.getErrorsMessages(); + if (fieldError === null) { + return acc; + } + return [...acc, fieldError]; + }, [] as string[]); + }; + + const isFieldValid = (field: FieldHook) => + field.getErrorsMessages() === null && !field.isValidating; + + const updateFormValidity = () => { + const fieldsArray = fieldsToArray(); + const areAllFieldsValidated = fieldsArray.every(field => field.isValidated); + + if (!areAllFieldsValidated) { + // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined" + return undefined; + } + + const isFormValid = fieldsArray.every(isFieldValid); + + setIsValid(isFormValid); + return isFormValid; }; - /** - * When a field value changes, validateFields() is called with the field name + any other fields - * declared in the "fieldsToValidateOnChange" (see the field config). - * - * When this method is called _without_ providing any fieldNames, we only need to validate fields that are pristine - * as the fields that are dirty have already been validated when their value changed. - */ const validateFields: FormHook['__validateFields'] = async fieldNames => { const fieldsToValidate = fieldNames - ? fieldNames.map(name => fieldsRefs.current[name]).filter(field => field !== undefined) - : fieldsToArray().filter(field => field.isPristine); // only validate fields that haven't been changed + .map(name => fieldsRefs.current[name]) + .filter(field => field !== undefined); - const formData = getFormData({ unflatten: false }); + if (fieldsToValidate.length === 0) { + // Nothing to validate + return { areFieldsValid: true, isFormValid: true }; + } + const formData = getFormData({ unflatten: false }); await Promise.all(fieldsToValidate.map(field => field.validate({ formData }))); - const isFormValid = fieldsToArray().every( - field => field.getErrorsMessages() === null && !field.isValidating - ); - setIsValid(isFormValid); + const isFormValid = updateFormValidity(); + const areFieldsValid = fieldsToValidate.every(isFieldValid); - return isFormValid; + return { areFieldsValid, isFormValid }; + }; + + const validateAllFields = async (): Promise => { + const fieldsToValidate = fieldsToArray().filter(field => !field.isValidated); + + if (fieldsToValidate.length === 0) { + // Nothing left to validate, all fields are already validated. + return isValid!; + } + + const { isFormValid } = await validateFields(fieldsToValidate.map(field => field.path)); + + return isFormValid!; }; const addField: FormHook['__addField'] = field => { fieldsRefs.current[field.path] = field; - // Only update the formData if the path does not exist (it is the _first_ time - // the field is added), to avoid entering an infinite loop when the form is re-rendered. if (!{}.hasOwnProperty.call(formData$.current.value, field.path)) { - updateFormDataAt(field.path, field.__serializeOutput()); + const fieldValue = field.__serializeOutput(); + updateFormDataAt(field.path, fieldValue); } }; @@ -143,10 +201,16 @@ export function useForm( }; const setFieldValue: FormHook['setFieldValue'] = (fieldName, value) => { + if (fieldsRefs.current[fieldName] === undefined) { + return; + } fieldsRefs.current[fieldName].setValue(value); }; const setFieldErrors: FormHook['setFieldErrors'] = (fieldName, errors) => { + if (fieldsRefs.current[fieldName] === undefined) { + return; + } fieldsRefs.current[fieldName].setErrors(errors); }; @@ -167,20 +231,58 @@ export function useForm( } if (!isSubmitted) { - setSubmitted(true); // User has attempted to submit the form at least once + setIsSubmitted(true); // User has attempted to submit the form at least once } setSubmitting(true); - const isFormValid = await validateFields(); + const isFormValid = await validateAllFields(); const formData = serializer(getFormData() as T); if (onSubmit) { - await onSubmit(formData, isFormValid); + await onSubmit(formData, isFormValid!); } setSubmitting(false); - return { data: formData, isValid: isFormValid }; + return { data: formData, isValid: isFormValid! }; + }; + + const subscribe: FormHook['subscribe'] = handler => { + const format = () => serializer(getFormData() as T); + + const subscription = formData$.current.subscribe(raw => { + if (!isUnmounted.current) { + handler({ isValid, data: { raw, format }, validate: validateAllFields }); + } + }); + + formUpdateSubscribers.current.push(subscription); + return subscription; + }; + + /** + * Reset all the fields of the form to their default values + * and reset all the states to their original value. + */ + const reset: FormHook['reset'] = (resetOptions = { resetValues: true }) => { + const { resetValues = true } = resetOptions; + const currentFormData = { ...formData$.current.value } as FormData; + Object.entries(fieldsRefs.current).forEach(([path, field]) => { + // By resetting the form, some field might be unmounted. In order + // to avoid a race condition, we check that the field still exists. + const isFieldMounted = fieldsRefs.current[path] !== undefined; + if (isFieldMounted) { + const fieldValue = field.reset({ resetValue: resetValues }); + currentFormData[path] = fieldValue; + } + }); + if (resetValues) { + formData$.current.next(currentFormData as T); + } + + setIsSubmitted(false); + setSubmitting(false); + setIsValid(undefined); }; const form: FormHook = { @@ -188,11 +290,14 @@ export function useForm( isSubmitting, isValid, submit: submitForm, + subscribe, setFieldValue, setFieldErrors, getFields, getFormData, + getErrors, getFieldDefaultValue, + reset, __options: formOptions, __formData$: formData$, __updateFormDataAt: updateFormDataAt, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts index 4c0169cb526e2..7365f234d39ed 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/subject.ts @@ -51,7 +51,9 @@ export class Subject { } next(value: T) { - this.value = value; - this.callbacks.forEach(fn => fn(value)); + if (value !== this.value) { + this.value = value; + this.callbacks.forEach(fn => fn(value)); + } } } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts index 62867a0c07a6b..65cd7792a0189 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts @@ -33,7 +33,7 @@ export const flattenObject = ( ): Record => Object.entries(object).reduce((acc, [key, value]) => { const updatedPaths = [...paths, key]; - if (value !== null && typeof value === 'object') { + if (value !== null && !Array.isArray(value) && typeof value === 'object') { return flattenObject(value, to, updatedPaths); } acc[updatedPaths.join('.')] = value; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 9946020132354..8dc1e59b40c34 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -18,40 +18,47 @@ */ import { ReactNode, ChangeEvent, FormEvent, MouseEvent, MutableRefObject } from 'react'; -import { Subject } from './lib'; +import { Subject, Subscription } from './lib'; // This type will convert all optional property to required ones // Comes from https://github.com/microsoft/TypeScript/issues/15012#issuecomment-365453623 -type Required = T extends object ? { [P in keyof T]-?: NonNullable } : T; +type Required = T extends FormData ? { [P in keyof T]-?: NonNullable } : T; -export interface FormHook { +export interface FormHook { readonly isSubmitted: boolean; readonly isSubmitting: boolean; - readonly isValid: boolean; + readonly isValid: boolean | undefined; submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; + subscribe: (handler: OnUpdateHandler) => Subscription; setFieldValue: (fieldName: string, value: FieldValue) => void; setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; getFields: () => FieldsMap; getFormData: (options?: { unflatten?: boolean }) => T; getFieldDefaultValue: (fieldName: string) => unknown; + /* Returns a list of all errors in the form */ + getErrors: () => string[]; + reset: (options?: { resetValues?: boolean }) => void; readonly __options: Required; readonly __formData$: MutableRefObject>; __addField: (field: FieldHook) => void; __removeField: (fieldNames: string | string[]) => void; - __validateFields: (fieldNames?: string[]) => Promise; + __validateFields: ( + fieldNames: string[] + ) => Promise<{ areFieldsValid: boolean; isFormValid: boolean | undefined }>; __updateFormDataAt: (field: string, value: unknown) => T; __readFieldConfigFromSchema: (fieldName: string) => FieldConfig; } -export interface FormSchema { +export interface FormSchema { [key: string]: FormSchemaEntry; } -type FormSchemaEntry = + +type FormSchemaEntry = | FieldConfig | Array> | { [key: string]: FieldConfig | Array> | FormSchemaEntry }; -export interface FormConfig { +export interface FormConfig { onSubmit?: (data: T, isFormValid: boolean) => void; schema?: FormSchema; defaultValue?: Partial; @@ -60,6 +67,17 @@ export interface FormConfig { options?: FormOptions; } +export interface OnFormUpdateArg { + data: { + raw: { [key: string]: any }; + format: () => T; + }; + validate: () => Promise; + isValid?: boolean; +} + +export type OnUpdateHandler = (arg: OnFormUpdateArg) => void; + export interface FormOptions { errorDisplayDelay?: number; /** @@ -78,6 +96,7 @@ export interface FieldHook { readonly errors: ValidationError[]; readonly isPristine: boolean; readonly isValidating: boolean; + readonly isValidated: boolean; readonly isChangingValue: boolean; readonly form: FormHook; getErrorsMessages: (args?: { @@ -93,16 +112,17 @@ export interface FieldHook { value?: unknown; validationType?: string; }) => FieldValidateResponse | Promise; + reset: (options?: { resetValue: boolean }) => unknown; __serializeOutput: (rawValue?: unknown) => unknown; } -export interface FieldConfig { +export interface FieldConfig { readonly path?: string; readonly label?: string; readonly labelAppend?: string | ReactNode; readonly helpText?: string | ReactNode; readonly type?: HTMLInputElement['type']; - readonly defaultValue?: unknown; + readonly defaultValue?: ValueType; readonly validations?: Array>; readonly formatters?: FormatterFunc[]; readonly deserializer?: SerializerFunc; @@ -124,13 +144,17 @@ export interface ValidationError { [key: string]: any; } -export type ValidationFunc = (data: { +export interface ValidationFuncArg { path: string; - value: unknown; + value: V; form: FormHook; formData: T; errors: readonly ValidationError[]; -}) => ValidationError | void | undefined | Promise | void | undefined>; +} + +export type ValidationFunc = ( + data: ValidationFuncArg +) => ValidationError | void | undefined | Promise | void | undefined>; export interface FieldValidateResponse { isValid: boolean; @@ -143,13 +167,13 @@ export interface FormData { [key: string]: any; } -type FormatterFunc = (value: any) => unknown; +type FormatterFunc = (value: any, formData: FormData) => unknown; // We set it as unknown as a form field can be any of any type // string | number | boolean | string[] ... type FieldValue = unknown; -export interface ValidationConfig { +export interface ValidationConfig { validator: ValidationFunc; type?: string; exitOnFail?: boolean; diff --git a/src/plugins/es_ui_shared/static/validators/number/greater_than.ts b/src/plugins/es_ui_shared/static/validators/number/greater_than.ts new file mode 100644 index 0000000000000..fa9024204e727 --- /dev/null +++ b/src/plugins/es_ui_shared/static/validators/number/greater_than.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const isNumberGreaterThan = (than: number, allowEquality = false) => (value: number) => + allowEquality ? value >= than : value > than; diff --git a/src/plugins/es_ui_shared/static/validators/number/index.ts b/src/plugins/es_ui_shared/static/validators/number/index.ts new file mode 100644 index 0000000000000..64a0cdd1b5a1d --- /dev/null +++ b/src/plugins/es_ui_shared/static/validators/number/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './greater_than'; + +export * from './smaller_than'; diff --git a/src/plugins/es_ui_shared/static/validators/number/smaller_than.ts b/src/plugins/es_ui_shared/static/validators/number/smaller_than.ts new file mode 100644 index 0000000000000..50b43890ebf05 --- /dev/null +++ b/src/plugins/es_ui_shared/static/validators/number/smaller_than.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const isNumberSmallerThan = (than: number, allowEquality = false) => (value: number) => + allowEquality ? value <= than : value < than; diff --git a/src/plugins/es_ui_shared/static/validators/string/index.ts b/src/plugins/es_ui_shared/static/validators/string/index.ts index 0ddf6fdfc33e4..1e80ca0700637 100644 --- a/src/plugins/es_ui_shared/static/validators/string/index.ts +++ b/src/plugins/es_ui_shared/static/validators/string/index.ts @@ -25,3 +25,4 @@ export * from './is_empty'; export * from './is_url'; export * from './starts_with'; export * from './is_json'; +export * from './is_lowercase'; diff --git a/src/plugins/es_ui_shared/static/validators/string/is_lowercase.ts b/src/plugins/es_ui_shared/static/validators/string/is_lowercase.ts new file mode 100644 index 0000000000000..3d765a750a81a --- /dev/null +++ b/src/plugins/es_ui_shared/static/validators/string/is_lowercase.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const isLowerCaseString = (value: string) => value.toLowerCase() === value; diff --git a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap b/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap index fb56bf0e4255e..870dbdc533267 100644 --- a/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap +++ b/src/plugins/kibana_react/public/field_icon/__snapshots__/field_icon.test.tsx.snap @@ -11,7 +11,7 @@ exports[`FieldIcon renders a blackwhite icon for a string 1`] = ` exports[`FieldIcon renders a colored icon for a number 1`] = ` @@ -20,7 +20,7 @@ exports[`FieldIcon renders a colored icon for a number 1`] = ` exports[`FieldIcon renders an icon for an unknown type 1`] = ` @@ -30,7 +30,7 @@ exports[`FieldIcon renders with className if provided 1`] = ` diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 1eac93c8538e4..96efd952e6ba2 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.0.0", + "@elastic/eui": "18.2.0", "react": "^16.12.0", "react-dom": "^16.12.0" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 1bfb1e8ba4bca..7693d6f9c07bc 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.0.0", + "@elastic/eui": "18.2.0", "react": "^16.12.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 6d6b04fba889c..bf58535e57994 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.0.0", + "@elastic/eui": "18.2.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index 964adacb2ac09..98dd9ab51da96 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.0.0", + "@elastic/eui": "18.2.0", "react": "^16.12.0" }, "scripts": { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index bc020815cc9cb..d69fa5d895b9e 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -26,7 +26,7 @@ interface CytoscapeProps { children?: ReactNode; elements: cytoscape.ElementDefinition[]; serviceName?: string; - style: CSSProperties; + style?: CSSProperties; } function useCytoscape(options: cytoscape.CytoscapeOptions) { @@ -69,8 +69,8 @@ export function Cytoscape({ // Set up cytoscape event handlers useEffect(() => { - if (cy) { - cy.on('data', event => { + const dataHandler: cytoscape.EventHandler = event => { + if (cy) { // Add the "primary" class to the node if its id matches the serviceName. if (cy.nodes().length > 0 && serviceName) { cy.nodes().removeClass('primary'); @@ -80,8 +80,30 @@ export function Cytoscape({ if (event.cy.elements().length > 0) { cy.layout(cytoscapeOptions.layout as cytoscape.LayoutOptions).run(); } - }); + } + }; + const mouseoverHandler: cytoscape.EventHandler = event => { + event.target.addClass('hover'); + event.target.connectedEdges().addClass('nodeHover'); + }; + const mouseoutHandler: cytoscape.EventHandler = event => { + event.target.removeClass('hover'); + event.target.connectedEdges().removeClass('nodeHover'); + }; + + if (cy) { + cy.on('data', dataHandler); + cy.on('mouseover', 'edge, node', mouseoverHandler); + cy.on('mouseout', 'edge, node', mouseoutHandler); } + + return () => { + if (cy) { + cy.removeListener('data', undefined, dataHandler); + cy.removeListener('mouseover', 'edge, node', mouseoverHandler); + cy.removeListener('mouseout', 'edge, node', mouseoutHandler); + } + }; }, [cy, serviceName]); return ( diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx new file mode 100644 index 0000000000000..a8c45c83a382a --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Buttons.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @elastic/eui/href-or-on-click */ + +import { EuiButton, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { MouseEvent } from 'react'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { getAPMHref } from '../../../shared/Links/apm/APMLink'; + +interface ButtonsProps { + focusedServiceName?: string; + onFocusClick?: (event: MouseEvent) => void; + selectedNodeServiceName: string; +} + +export function Buttons({ + focusedServiceName, + onFocusClick = () => {}, + selectedNodeServiceName +}: ButtonsProps) { + const currentSearch = useUrlParams().urlParams.kuery ?? ''; + const detailsUrl = getAPMHref( + `/services/${selectedNodeServiceName}/transactions`, + currentSearch + ); + const focusUrl = getAPMHref( + `/services/${selectedNodeServiceName}/service-map`, + currentSearch + ); + + const isAlreadyFocused = focusedServiceName === selectedNodeServiceName; + + return ( + <> + + + {i18n.translate('xpack.apm.serviceMap.serviceDetailsButtonText', { + defaultMessage: 'Service Details' + })} + + + + + {i18n.translate('xpack.apm.serviceMap.focusMapButtonText', { + defaultMessage: 'Focus map' + })} + + + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx new file mode 100644 index 0000000000000..1c5443e404f9b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import styled from 'styled-components'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; + +const ItemRow = styled.div` + line-height: 2; +`; + +const ItemTitle = styled.dt` + color: ${lightTheme.textColors.subdued}; +`; + +const ItemDescription = styled.dd``; + +interface InfoProps { + type: string; + subtype?: string; +} + +export function Info({ type, subtype }: InfoProps) { + const listItems = [ + { + title: i18n.translate('xpack.apm.serviceMap.typePopoverMetric', { + defaultMessage: 'Type' + }), + description: type + }, + { + title: i18n.translate('xpack.apm.serviceMap.subtypePopoverMetric', { + defaultMessage: 'Subtype' + }), + description: subtype + } + ]; + + return ( + <> + {listItems.map( + ({ title, description }) => + description && ( + + {title} + {description} + + ) + )} + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx new file mode 100644 index 0000000000000..8ce6d9d57c4ac --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx @@ -0,0 +1,177 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiLoadingSpinner, + EuiFlexItem, + EuiBadge +} from '@elastic/eui'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { isNumber } from 'lodash'; +import React from 'react'; +import styled from 'styled-components'; +import { ServiceNodeMetrics } from '../../../../../server/lib/service_map/get_service_map_service_node_info'; +import { + asDuration, + asPercent, + toMicroseconds, + tpmUnit +} from '../../../../utils/formatters'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../hooks/useFetcher'; + +function LoadingSpinner() { + return ( + + + + ); +} + +const ItemRow = styled('tr')` + line-height: 2; +`; + +const ItemTitle = styled('td')` + color: ${lightTheme.textColors.subdued}; + padding-right: 1rem; +`; + +const ItemDescription = styled('td')` + text-align: right; +`; + +const na = i18n.translate('xpack.apm.serviceMap.NotAvailableMetric', { + defaultMessage: 'N/A' +}); + +interface MetricListProps { + serviceName: string; +} + +export function ServiceMetricList({ serviceName }: MetricListProps) { + const { + urlParams: { start, end, environment } + } = useUrlParams(); + + const { data = {} as ServiceNodeMetrics, status } = useFetcher( + callApmApi => { + if (serviceName && start && end) { + return callApmApi({ + pathname: '/api/apm/service-map/service/{serviceName}', + params: { + path: { + serviceName + }, + query: { + start, + end, + environment + } + } + }); + } + }, + [serviceName, start, end, environment], + { + preservePreviousData: false + } + ); + + const { + avgTransactionDuration, + avgRequestsPerMinute, + avgErrorsPerMinute, + avgCpuUsage, + avgMemoryUsage, + numInstances + } = data; + const isLoading = status === 'loading'; + + const listItems = [ + { + title: i18n.translate( + 'xpack.apm.serviceMap.avgTransDurationPopoverMetric', + { + defaultMessage: 'Trans. duration (avg.)' + } + ), + description: isNumber(avgTransactionDuration) + ? asDuration(toMicroseconds(avgTransactionDuration, 'milliseconds')) + : na + }, + { + title: i18n.translate( + 'xpack.apm.serviceMap.avgReqPerMinutePopoverMetric', + { + defaultMessage: 'Req. per minute (avg.)' + } + ), + description: isNumber(avgRequestsPerMinute) + ? `${avgRequestsPerMinute.toFixed(2)} ${tpmUnit('request')}` + : na + }, + { + title: i18n.translate( + 'xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric', + { + defaultMessage: 'Errors per minute (avg.)' + } + ), + description: avgErrorsPerMinute?.toFixed(2) ?? na + }, + { + title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverMetric', { + defaultMessage: 'CPU usage (avg.)' + }), + description: isNumber(avgCpuUsage) ? asPercent(avgCpuUsage, 1) : na + }, + { + title: i18n.translate( + 'xpack.apm.serviceMap.avgMemoryUsagePopoverMetric', + { + defaultMessage: 'Memory usage (avg.)' + } + ), + description: isNumber(avgMemoryUsage) ? asPercent(avgMemoryUsage, 1) : na + } + ]; + return isLoading ? ( + + ) : ( + <> + {numInstances && numInstances > 1 && ( + +
+ + {i18n.translate('xpack.apm.serviceMap.numInstancesMetric', { + values: { numInstances }, + defaultMessage: '{numInstances} instances' + })} + +
+
+ )} + + + + {listItems.map(({ title, description }) => ( + + {title} + {description} + + ))} + +
+ + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx new file mode 100644 index 0000000000000..dfb78aaa0214c --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Popover/index.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPopover, + EuiTitle +} from '@elastic/eui'; +import cytoscape from 'cytoscape'; +import React, { + CSSProperties, + useContext, + useEffect, + useState, + useCallback +} from 'react'; +import { CytoscapeContext } from '../Cytoscape'; +import { Buttons } from './Buttons'; +import { Info } from './Info'; +import { ServiceMetricList } from './ServiceMetricList'; + +const popoverMinWidth = 280; + +interface PopoverProps { + focusedServiceName?: string; +} + +export function Popover({ focusedServiceName }: PopoverProps) { + const cy = useContext(CytoscapeContext); + const [selectedNode, setSelectedNode] = useState< + cytoscape.NodeSingular | undefined + >(undefined); + const onFocusClick = useCallback(() => setSelectedNode(undefined), [ + setSelectedNode + ]); + + useEffect(() => { + const selectHandler: cytoscape.EventHandler = event => { + setSelectedNode(event.target); + }; + const unselectHandler: cytoscape.EventHandler = () => { + setSelectedNode(undefined); + }; + + if (cy) { + cy.on('select', 'node', selectHandler); + cy.on('unselect', 'node', unselectHandler); + cy.on('data viewport', unselectHandler); + } + + return () => { + if (cy) { + cy.removeListener('select', 'node', selectHandler); + cy.removeListener('unselect', 'node', unselectHandler); + cy.removeListener('data viewport', undefined, unselectHandler); + } + }; + }, [cy]); + + const renderedHeight = selectedNode?.renderedHeight() ?? 0; + const renderedWidth = selectedNode?.renderedWidth() ?? 0; + const { x, y } = selectedNode?.renderedPosition() ?? { x: 0, y: 0 }; + const isOpen = !!selectedNode; + const selectedNodeServiceName: string = selectedNode?.data('id'); + const isService = selectedNode?.data('type') === 'service'; + const triggerStyle: CSSProperties = { + background: 'transparent', + height: renderedHeight, + position: 'absolute', + width: renderedWidth + }; + const trigger =
; + + const zoom = cy?.zoom() ?? 1; + const height = selectedNode?.height() ?? 0; + const translateY = y - (zoom + 1) * (height / 2); + const popoverStyle: CSSProperties = { + position: 'absolute', + transform: `translate(${x}px, ${translateY}px)` + }; + const data = selectedNode?.data() ?? {}; + const label = data.label || selectedNodeServiceName; + + return ( + {}} + isOpen={isOpen} + style={popoverStyle} + > + + + +

{label}

+
+ +
+ + + {isService ? ( + + ) : ( + + )} + + {isService && ( + + )} +
+
+ ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index d4e792ccf761b..1a6247388a655 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import cytoscape from 'cytoscape'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import { icons, defaultIcon } from './icons'; +import cytoscape from 'cytoscape'; +import { defaultIcon, iconForNode } from './icons'; const layout = { name: 'dagre', @@ -13,8 +13,8 @@ const layout = { rankDir: 'LR' }; -function isDatabaseOrExternal(agentName: string) { - return !agentName; +function isService(el: cytoscape.NodeSingular) { + return el.data('type') === 'service'; } const style: cytoscape.Stylesheet[] = [ @@ -27,11 +27,11 @@ const style: cytoscape.Stylesheet[] = [ // // @ts-ignore 'background-image': (el: cytoscape.NodeSingular) => - icons[el.data('agentName')] || defaultIcon, + iconForNode(el) ?? defaultIcon, 'background-height': (el: cytoscape.NodeSingular) => - isDatabaseOrExternal(el.data('agentName')) ? '40%' : '80%', + isService(el) ? '80%' : '40%', 'background-width': (el: cytoscape.NodeSingular) => - isDatabaseOrExternal(el.data('agentName')) ? '40%' : '80%', + isService(el) ? '80%' : '40%', 'border-color': (el: cytoscape.NodeSingular) => el.hasClass('primary') ? theme.euiColorSecondary @@ -47,7 +47,7 @@ const style: cytoscape.Stylesheet[] = [ 'min-zoomed-font-size': theme.euiSizeL, 'overlay-opacity': 0, shape: (el: cytoscape.NodeSingular) => - isDatabaseOrExternal(el.data('agentName')) ? 'diamond' : 'ellipse', + isService(el) ? 'ellipse' : 'diamond', 'text-background-color': theme.euiColorLightestShade, 'text-background-opacity': 0, 'text-background-padding': theme.paddingSizes.xs, @@ -90,7 +90,6 @@ const style: cytoscape.Stylesheet[] = [ export const cytoscapeOptions: cytoscape.CytoscapeOptions = { autoungrabify: true, - autounselectify: true, boxSelectionEnabled: false, layout, maxZoom: 3, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts index c9caa27af41c5..106e9a1d82f29 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts @@ -101,7 +101,17 @@ export function getCytoscapeElements( `/services/${node['service.name']}/service-map`, search ), - agentName: node['agent.name'] || node['agent.name'] + agentName: node['agent.name'] || node['agent.name'], + type: 'service' + }; + } + + if ('span.type' in node) { + data = { + // For nodes with span.type "db", convert it to "database". Otherwise leave it as-is. + type: node['span.type'] === 'db' ? 'database' : node['span.type'], + // Externals should not have a subtype so make it undefined if the type is external. + subtype: node['span.type'] !== 'external' && node['span.subtype'] }; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts index d5cfb49e458c6..722f64c6a7e58 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -5,7 +5,9 @@ */ import theme from '@elastic/eui/dist/eui_theme_light.json'; +import cytoscape from 'cytoscape'; import databaseIcon from './icons/database.svg'; +import documentsIcon from './icons/documents.svg'; import globeIcon from './icons/globe.svg'; function getAvatarIcon( @@ -24,10 +26,16 @@ function getAvatarIcon( } // The colors here are taken from the logos of the corresponding technologies -export const icons: { [key: string]: string } = { +const icons: { [key: string]: string } = { + cache: databaseIcon, database: databaseIcon, - dotnet: getAvatarIcon('.N', '#8562AD'), external: globeIcon, + messaging: documentsIcon, + resource: globeIcon +}; + +const serviceIcons: { [key: string]: string } = { + dotnet: getAvatarIcon('.N', '#8562AD'), go: getAvatarIcon('Go', '#00A9D6'), java: getAvatarIcon('Jv', '#41717E'), 'js-base': getAvatarIcon('JS', '#F0DB4E', theme.euiTextColor), @@ -37,3 +45,12 @@ export const icons: { [key: string]: string } = { }; export const defaultIcon = getAvatarIcon(); + +export function iconForNode(node: cytoscape.NodeSingular) { + const type = node.data('type'); + if (type === 'service') { + return serviceIcons[node.data('agentName') as string]; + } else { + return icons[type]; + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg new file mode 100644 index 0000000000000..b0648d14f20ba --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/icons/documents.svg @@ -0,0 +1 @@ + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index d3cc2b14e2c68..a8e6f964f4d0c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,31 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiButton } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { i18n } from '@kbn/i18n'; +import { ElementDefinition } from 'cytoscape'; +import { find, isEqual } from 'lodash'; import React, { - useMemo, + useCallback, useEffect, - useState, + useMemo, useRef, - useCallback + useState } from 'react'; -import { find, isEqual } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { EuiButton } from '@elastic/eui'; -import { ElementDefinition } from 'cytoscape'; import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { ServiceMapAPIResponse } from '../../../../server/lib/service_map/get_service_map'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { useCallApmApi } from '../../../hooks/useCallApmApi'; +import { useDeepObjectIdentity } from '../../../hooks/useDeepObjectIdentity'; import { useLicense } from '../../../hooks/useLicense'; +import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { Controls } from './Controls'; import { Cytoscape } from './Cytoscape'; -import { PlatinumLicensePrompt } from './PlatinumLicensePrompt'; -import { useCallApmApi } from '../../../hooks/useCallApmApi'; -import { useDeepObjectIdentity } from '../../../hooks/useDeepObjectIdentity'; -import { useLocation } from '../../../hooks/useLocation'; -import { LoadingOverlay } from './LoadingOverlay'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { getCytoscapeElements } from './get_cytoscape_elements'; +import { LoadingOverlay } from './LoadingOverlay'; +import { PlatinumLicensePrompt } from './PlatinumLicensePrompt'; +import { Popover } from './Popover'; interface ServiceMapProps { serviceName?: string; @@ -205,6 +206,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { style={cytoscapeDivStyle} > + ) : ( diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 9b2a2c8f2490a..0ddf23cb932fb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -295,7 +295,7 @@ NodeList [ class="euiTableCellContent euiTableCellContent--overflowingContent" > , }, Object { - "color": "#fae181", + "color": "#d6bf57", "disabled": undefined, "onClick": [Function], "text": @@ -22,7 +22,7 @@ Array [ , }, Object { - "color": "#f19f58", + "color": "#da8b45", "disabled": undefined, "onClick": [Function], "text": @@ -442,7 +442,7 @@ Array [ style={ Object { "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeDasharray": undefined, "strokeWidth": undefined, } @@ -463,9 +463,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -480,9 +480,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -497,9 +497,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -514,9 +514,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -531,9 +531,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -548,9 +548,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -565,9 +565,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -582,9 +582,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -599,9 +599,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -616,9 +616,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -633,9 +633,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -650,9 +650,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -667,9 +667,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -684,9 +684,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -701,9 +701,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -718,9 +718,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -735,9 +735,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -752,9 +752,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -769,9 +769,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -786,9 +786,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -803,9 +803,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -820,9 +820,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -837,9 +837,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -854,9 +854,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -871,9 +871,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -888,9 +888,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -905,9 +905,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -922,9 +922,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -939,9 +939,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -956,9 +956,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -973,9 +973,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -995,7 +995,7 @@ Array [ style={ Object { "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeDasharray": undefined, "strokeWidth": undefined, } @@ -1016,9 +1016,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1033,9 +1033,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1050,9 +1050,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1067,9 +1067,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1084,9 +1084,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1101,9 +1101,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1118,9 +1118,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1135,9 +1135,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1152,9 +1152,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1169,9 +1169,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1186,9 +1186,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1203,9 +1203,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1220,9 +1220,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1237,9 +1237,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1254,9 +1254,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1271,9 +1271,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1288,9 +1288,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1305,9 +1305,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1322,9 +1322,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1339,9 +1339,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1356,9 +1356,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1373,9 +1373,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1390,9 +1390,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1407,9 +1407,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1424,9 +1424,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1441,9 +1441,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1458,9 +1458,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1475,9 +1475,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1492,9 +1492,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1509,9 +1509,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -1526,9 +1526,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -2659,7 +2659,7 @@ Array [ width: 11px; height: 11px; margin-right: 5.5px; - background: #fae181; + background: #d6bf57; border-radius: 100%; } @@ -2667,7 +2667,7 @@ Array [ width: 11px; height: 11px; margin-right: 5.5px; - background: #f19f58; + background: #da8b45; border-radius: 100%; } @@ -2764,14 +2764,14 @@ Array [ onClick={[Function]} > @@ -2798,14 +2798,14 @@ Array [ onClick={[Function]} > @@ -2854,12 +2854,12 @@ Array [ "value": 438704.4, }, Object { - "color": "#fae181", + "color": "#d6bf57", "text": "95th", "value": 1557383.999999999, }, Object { - "color": "#f19f58", + "color": "#da8b45", "text": "99th", "value": 1820377.1200000006, }, @@ -2899,7 +2899,7 @@ Array [ width: 8px; height: 8px; margin-right: 4px; - background: #fae181; + background: #d6bf57; border-radius: 100%; } @@ -2907,7 +2907,7 @@ Array [ width: 8px; height: 8px; margin-right: 4px; - background: #f19f58; + background: #da8b45; border-radius: 100%; } @@ -3378,7 +3378,7 @@ Array [ style={ Object { "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeDasharray": undefined, "strokeWidth": undefined, } @@ -3399,9 +3399,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3416,9 +3416,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3433,9 +3433,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3450,9 +3450,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3467,9 +3467,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3484,9 +3484,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3501,9 +3501,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3518,9 +3518,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3535,9 +3535,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3552,9 +3552,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3569,9 +3569,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3586,9 +3586,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3603,9 +3603,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3620,9 +3620,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3637,9 +3637,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3654,9 +3654,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3671,9 +3671,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3688,9 +3688,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3705,9 +3705,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3722,9 +3722,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3739,9 +3739,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3756,9 +3756,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3773,9 +3773,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3790,9 +3790,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3807,9 +3807,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3824,9 +3824,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3841,9 +3841,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3858,9 +3858,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3875,9 +3875,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3892,9 +3892,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3909,9 +3909,9 @@ Array [ r={0.5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -3931,7 +3931,7 @@ Array [ style={ Object { "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeDasharray": undefined, "strokeWidth": undefined, } @@ -3952,9 +3952,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -3969,9 +3969,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -3986,9 +3986,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4003,9 +4003,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4020,9 +4020,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4037,9 +4037,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4054,9 +4054,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4071,9 +4071,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4088,9 +4088,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4105,9 +4105,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4122,9 +4122,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4139,9 +4139,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4156,9 +4156,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4173,9 +4173,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4190,9 +4190,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4207,9 +4207,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4224,9 +4224,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4241,9 +4241,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4258,9 +4258,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4275,9 +4275,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4292,9 +4292,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4309,9 +4309,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4326,9 +4326,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4343,9 +4343,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4360,9 +4360,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4377,9 +4377,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4394,9 +4394,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4411,9 +4411,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4428,9 +4428,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4445,9 +4445,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -4462,9 +4462,9 @@ Array [ r={0.5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -5072,9 +5072,9 @@ Array [ r={5} style={ Object { - "fill": "#f19f58", + "fill": "#da8b45", "opacity": 1, - "stroke": "#f19f58", + "stroke": "#da8b45", "strokeWidth": 1, } } @@ -5089,9 +5089,9 @@ Array [ r={5} style={ Object { - "fill": "#fae181", + "fill": "#d6bf57", "opacity": 1, - "stroke": "#fae181", + "stroke": "#d6bf57", "strokeWidth": 1, } } @@ -5204,7 +5204,7 @@ Array [ className="c3" > @@ -5220,14 +5220,14 @@ Array [ fontSize="12px" > @@ -5250,7 +5250,7 @@ Array [ className="c3" > @@ -5266,14 +5266,14 @@ Array [ fontSize="12px" > @@ -5838,7 +5838,7 @@ Array [ width: 11px; height: 11px; margin-right: 5.5px; - background: #fae181; + background: #d6bf57; border-radius: 100%; } @@ -5846,7 +5846,7 @@ Array [ width: 11px; height: 11px; margin-right: 5.5px; - background: #f19f58; + background: #da8b45; border-radius: 100%; } @@ -5943,14 +5943,14 @@ Array [ onClick={[Function]} > @@ -5977,14 +5977,14 @@ Array [ onClick={[Function]} > diff --git a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts b/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts index 1218bc726c3b7..252c49cc09fb9 100644 --- a/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts +++ b/x-pack/legacy/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts @@ -67,7 +67,7 @@ describe('chartSelectors', () => { type: 'linemark' }, { - color: '#fae181', + color: '#d6bf57', data: [ { x: 0, y: 200 }, { x: 1000, y: 300 } @@ -77,7 +77,7 @@ describe('chartSelectors', () => { type: 'linemark' }, { - color: '#f19f58', + color: '#da8b45', data: [ { x: 0, y: 300 }, { x: 1000, y: 400 } diff --git a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index 8c6ed2ebcec75..870660c429ca3 100644 --- a/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -43,7 +43,7 @@ const chartBase: ChartBase = { series }; -const percentUsedScript = { +export const percentMemoryUsedScript = { lang: 'expression', source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']` }; @@ -59,8 +59,8 @@ export async function getMemoryChartData( serviceNodeName, chartBase, aggs: { - memoryUsedAvg: { avg: { script: percentUsedScript } }, - memoryUsedMax: { max: { script: percentUsedScript } } + memoryUsedAvg: { avg: { script: percentMemoryUsedScript } }, + memoryUsedMax: { max: { script: percentMemoryUsedScript } } }, additionalFilters: [ { diff --git a/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts index ea9af12ac7f9a..d3711e9582d15 100644 --- a/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts +++ b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts @@ -163,7 +163,8 @@ export async function getServiceMapFromTraceIds({ } /* if there is an outgoing span, create a new path */ - if (event['span.type'] == 'external' || event['span.type'] == 'messaging') { + if (event['destination.address'] != null + && event['destination.address'] != '') { def outgoingLocation = getDestination(event); def outgoingPath = new ArrayList(basePath); outgoingPath.add(outgoingLocation); diff --git a/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts new file mode 100644 index 0000000000000..6c4d540103cec --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -0,0 +1,267 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { ESFilter } from '../../../typings/elasticsearch'; +import { rangeFilter } from '../helpers/range_filter'; +import { + PROCESSOR_EVENT, + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_DURATION, + METRIC_SYSTEM_CPU_PERCENT, + METRIC_SYSTEM_FREE_MEMORY, + METRIC_SYSTEM_TOTAL_MEMORY, + SERVICE_NODE_NAME +} from '../../../common/elasticsearch_fieldnames'; +import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; +import { PromiseReturnType } from '../../../typings/common'; + +interface Options { + setup: Setup & SetupTimeRange; + environment?: string; + serviceName: string; +} + +interface TaskParameters { + setup: Setup; + minutes: number; + filter: ESFilter[]; +} + +export type ServiceNodeMetrics = PromiseReturnType< + typeof getServiceMapServiceNodeInfo +>; + +export async function getServiceMapServiceNodeInfo({ + serviceName, + environment, + setup +}: Options & { serviceName: string; environment?: string }) { + const { start, end } = setup; + + const filter: ESFilter[] = [ + { range: rangeFilter(start, end) }, + { term: { [SERVICE_NAME]: serviceName } }, + ...(environment + ? [{ term: { [SERVICE_ENVIRONMENT]: SERVICE_ENVIRONMENT } }] + : []) + ]; + + const minutes = Math.abs((end - start) / (1000 * 60)); + + const taskParams = { + setup, + minutes, + filter + }; + + const [ + errorMetrics, + transactionMetrics, + cpuMetrics, + memoryMetrics, + instanceMetrics + ] = await Promise.all([ + getErrorMetrics(taskParams), + getTransactionMetrics(taskParams), + getCpuMetrics(taskParams), + getMemoryMetrics(taskParams), + getNumInstances(taskParams) + ]); + + return { + ...errorMetrics, + ...transactionMetrics, + ...cpuMetrics, + ...memoryMetrics, + ...instanceMetrics + }; +} + +async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) { + const { client, indices } = setup; + + const response = await client.search({ + index: indices['apm_oss.errorIndices'], + body: { + size: 0, + query: { + bool: { + filter: filter.concat({ + term: { + [PROCESSOR_EVENT]: 'error' + } + }) + } + }, + track_total_hits: true + } + }); + + return { + avgErrorsPerMinute: + response.hits.total.value > 0 ? response.hits.total.value / minutes : null + }; +} + +async function getTransactionMetrics({ + setup, + filter, + minutes +}: TaskParameters) { + const { indices, client } = setup; + + const response = await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + size: 1, + query: { + bool: { + filter: filter.concat({ + term: { + [PROCESSOR_EVENT]: 'transaction' + } + }) + } + }, + track_total_hits: true, + aggs: { + duration: { + avg: { + field: TRANSACTION_DURATION + } + } + } + } + }); + + return { + avgTransactionDuration: response.aggregations?.duration.value, + avgRequestsPerMinute: + response.hits.total.value > 0 ? response.hits.total.value / minutes : null + }; +} + +async function getCpuMetrics({ setup, filter }: TaskParameters) { + const { indices, client } = setup; + + const response = await client.search({ + index: indices['apm_oss.metricsIndices'], + body: { + size: 0, + query: { + bool: { + filter: filter.concat([ + { + term: { + [PROCESSOR_EVENT]: 'metric' + } + }, + { + exists: { + field: METRIC_SYSTEM_CPU_PERCENT + } + } + ]) + } + }, + aggs: { + avgCpuUsage: { + avg: { + field: METRIC_SYSTEM_CPU_PERCENT + } + } + } + } + }); + + return { + avgCpuUsage: response.aggregations?.avgCpuUsage.value + }; +} + +async function getMemoryMetrics({ setup, filter }: TaskParameters) { + const { client, indices } = setup; + const response = await client.search({ + index: indices['apm_oss.metricsIndices'], + body: { + query: { + bool: { + filter: filter.concat([ + { + term: { + [PROCESSOR_EVENT]: 'metric' + } + }, + { + exists: { + field: METRIC_SYSTEM_FREE_MEMORY + } + }, + { + exists: { + field: METRIC_SYSTEM_TOTAL_MEMORY + } + } + ]) + } + }, + aggs: { + avgMemoryUsage: { + avg: { + script: percentMemoryUsedScript + } + } + } + } + }); + + return { + avgMemoryUsage: response.aggregations?.avgMemoryUsage.value + }; +} + +async function getNumInstances({ setup, filter }: TaskParameters) { + const { client, indices } = setup; + const response = await client.search({ + index: indices['apm_oss.transactionIndices'], + body: { + query: { + bool: { + filter: filter.concat([ + { + term: { + [PROCESSOR_EVENT]: 'transaction' + } + }, + { + exists: { + field: SERVICE_NODE_NAME + } + }, + { + exists: { + field: METRIC_SYSTEM_TOTAL_MEMORY + } + } + ]) + } + }, + aggs: { + instances: { + cardinality: { + field: SERVICE_NODE_NAME + } + } + } + } + }); + + return { + numInstances: response.aggregations?.instances.value || 1 + }; +} diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts index 870b02fa7ba6d..476928a5bcb63 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.test.ts @@ -70,7 +70,7 @@ describe('getTransactionBreakdown', () => { expect(response.kpis[0]).toEqual({ name: 'app', - color: '#5bbaa0', + color: '#54b399', percentage: 0.5408550899466306 }); diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts index a9a8241da39d1..cf27d20c24360 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts @@ -58,7 +58,7 @@ import { uiFiltersEnvironmentsRoute } from './ui_filters'; import { createApi } from './create_api'; -import { serviceMapRoute } from './service_map'; +import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; const createApmApi = () => { const api = createApi() @@ -123,7 +123,8 @@ const createApmApi = () => { .add(transactionByTraceIdRoute) // Service map - .add(serviceMapRoute); + .add(serviceMapRoute) + .add(serviceMapServiceNodeRoute); return api; }; diff --git a/x-pack/legacy/plugins/apm/server/routes/service_map.ts b/x-pack/legacy/plugins/apm/server/routes/service_map.ts index 94b176147f7a1..584598805f8b3 100644 --- a/x-pack/legacy/plugins/apm/server/routes/service_map.ts +++ b/x-pack/legacy/plugins/apm/server/routes/service_map.ts @@ -10,6 +10,7 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceMap } from '../lib/service_map/get_service_map'; +import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; export const serviceMapRoute = createRoute(() => ({ path: '/api/apm/service-map', @@ -32,3 +33,35 @@ export const serviceMapRoute = createRoute(() => ({ return getServiceMap({ setup, serviceName, environment, after }); } })); + +export const serviceMapServiceNodeRoute = createRoute(() => ({ + path: `/api/apm/service-map/service/{serviceName}`, + params: { + path: t.type({ + serviceName: t.string + }), + query: t.intersection([ + rangeRt, + t.partial({ + environment: t.string + }) + ]) + }, + handler: async ({ context, request }) => { + if (!context.config['xpack.apm.serviceMapEnabled']) { + throw Boom.notFound(); + } + const setup = await setupRequest(context, request); + + const { + query: { environment }, + path: { serviceName } + } = context.params; + + return getServiceMapServiceNodeInfo({ + setup, + serviceName, + environment + }); + } +})); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts index aca30780d77cd..4c535a42c3c44 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/chart.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { euiPaletteColorBlind } from '@elastic/eui'; import { TagFactory } from '../../../public/lib/tag'; import { TagStrings as strings } from '../../../i18n'; +const euiVisPalette = euiPaletteColorBlind(); export const chart: TagFactory = () => ({ name: strings.chart(), - color: '#FEB6DB', + color: euiVisPalette[4], }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts index d3d251026e9b0..5249856dec271 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { euiPaletteColorBlind } from '@elastic/eui'; import { TagFactory } from '../../../public/lib/tag'; import { TagStrings as strings } from '../../../i18n'; +const euiVisPalette = euiPaletteColorBlind(); + export const filter: TagFactory = () => ({ name: strings.filter(), - color: '#3185FC', + color: euiVisPalette[1], }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts index 325a531b219ee..36d66801ef681 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { euiPaletteColorBlind } from '@elastic/eui'; import { TagFactory } from '../../../public/lib/tag'; import { TagStrings as strings } from '../../../i18n'; +const euiVisPalette = euiPaletteColorBlind(); export const graphic: TagFactory = () => ({ name: strings.graphic(), - color: '#E6C220', + color: euiVisPalette[5], }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js index ac6447ffd9dc0..6a59a6795d45a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const proportion = () => ({ name: 'proportion', color: '#490092' }); +import { euiPaletteColorBlind } from '@elastic/eui'; +const euiVisPalette = euiPaletteColorBlind(); + +export const proportion = () => ({ name: 'proportion', color: euiVisPalette[3] }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts index e538b4bf53103..4d37ecfaa367a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { euiPaletteColorBlind } from '@elastic/eui'; import { TagFactory } from '../../../public/lib/tag'; - import { TagStrings as strings } from '../../../i18n'; +const euiVisPalette = euiPaletteColorBlind(); export const proportion: TagFactory = () => ({ name: strings.proportion(), - color: '#490092', + color: euiVisPalette[3], }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts index 5df30581cd070..8dfbe1cb21614 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/report.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { euiPaletteColorBlind } from '@elastic/eui'; import { TagFactory } from '../../../public/lib/tag'; import { TagStrings as strings } from '../../../i18n'; +const euiVisPalette = euiPaletteColorBlind(); export const report: TagFactory = () => ({ name: strings.report(), - color: '#DB1374', + color: euiVisPalette[2], }); diff --git a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot index efaa34001971e..a469f03a71e3e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot @@ -118,6 +118,7 @@ Array [