From c8765089924c5fd55feb6ecccd4773c14a23fd76 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 24 Jul 2019 13:53:47 +0300 Subject: [PATCH 1/7] Use index patterns service inside data plugin (rather than importing from ui/public) (#41639) (#41867) * Use index patterns from data plugin itself instead of importing from ui/public * Added mocks to field editor util tests --- .../public/filter/filter_bar/filter_bar.tsx | 2 +- .../filter/filter_bar/filter_editor/index.tsx | 2 +- .../lib/filter_editor_utils.test.ts | 19 ++++++++++++++++++- .../filter_editor/lib/filter_editor_utils.ts | 5 ++--- .../filter_editor/phrase_suggestor.tsx | 2 +- .../filter_editor/range_value_input.tsx | 2 +- .../public/filter/filter_bar/filter_item.tsx | 2 +- .../data/public/index_patterns/index.ts | 2 ++ .../index_patterns/index_patterns_service.ts | 9 ++++++++- .../query_bar/components/query_bar_input.tsx | 2 +- .../query_bar/lib/fetch_index_patterns.ts | 4 ++-- 11 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx index af0933326a052..1f115964709e9 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_bar.tsx @@ -32,7 +32,7 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { Component } from 'react'; import chrome from 'ui/chrome'; -import { IndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from '../../index_patterns'; import { FilterEditor } from './filter_editor'; import { FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx index 892ba2bcd9407..ebb6c433450a2 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/index.tsx @@ -35,7 +35,7 @@ import { FieldFilter, Filter } from '@kbn/es-query'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import React, { Component } from 'react'; -import { Field, IndexPattern } from 'ui/index_patterns'; +import { Field, IndexPattern } from '../../../index_patterns'; import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box'; import { buildCustomFilter, diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts index d3006ceb10a2c..8190146a4258f 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.test.ts @@ -18,7 +18,8 @@ */ import { FilterStateStore, toggleFilterNegated } from '@kbn/es-query'; -import { mockFields, mockIndexPattern } from 'ui/index_patterns/fixtures'; + +import { fixtures } from '../../../../index_patterns'; import { buildFilter, getFieldFromFilter, @@ -42,6 +43,22 @@ import { phraseFilter } from './fixtures/phrase_filter'; import { phrasesFilter } from './fixtures/phrases_filter'; import { rangeFilter } from './fixtures/range_filter'; +jest.mock('ui/kfetch', () => ({ + kfetch: () => {}, +})); + +jest.mock( + 'ui/notify', + () => ({ + toastNotifications: { + addWarning: () => {}, + }, + }), + { virtual: true } +); + +const { mockFields, mockIndexPattern } = fixtures; + describe('Filter editor utils', () => { describe('getQueryDslFromFilter', () => { it('should return query DSL without meta and $state', () => { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts index b1b456e482ac9..a4704e2b1d644 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/lib/filter_editor_utils.ts @@ -32,9 +32,8 @@ import { RangeFilter, } from '@kbn/es-query'; import { omit } from 'lodash'; -import { Field, IndexPattern } from 'ui/index_patterns'; -import { isFilterable } from 'ui/index_patterns/static_utils'; import Ipv4Address from 'ui/utils/ipv4_address'; +import { Field, IndexPattern, utils as indexPatternUtils } from '../../../../index_patterns'; import { FILTER_OPERATORS, Operator } from './filter_operators'; export function getIndexPatternFromFilter( @@ -59,7 +58,7 @@ export function getQueryDslFromFilter(filter: Filter) { } export function getFilterableFields(indexPattern: IndexPattern) { - return indexPattern.fields.filter(isFilterable); + return indexPattern.fields.filter(indexPatternUtils.isFilterable); } export function getOperatorOptions(field: Field) { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx index 75073def34281..9cfd3aa4fd331 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/phrase_suggestor.tsx @@ -19,8 +19,8 @@ import { Component } from 'react'; import chrome from 'ui/chrome'; -import { Field, IndexPattern } from 'ui/index_patterns'; import { getSuggestions } from 'ui/value_suggestions'; +import { Field, IndexPattern } from '../../../index_patterns'; const config = chrome.getUiSettingsClient(); export interface PhraseSuggestorProps { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx index 3aa955210fdf0..c436db8d4a560 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_editor/range_value_input.tsx @@ -23,7 +23,7 @@ import { get } from 'lodash'; import { Component } from 'react'; import React from 'react'; import { getDocLink } from 'ui/documentation_links'; -import { Field } from 'ui/index_patterns'; +import { Field } from '../../../index_patterns'; import { ValueInputType } from './value_input_type'; interface RangeParams { diff --git a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx index d4fd894b93b23..453dafdb29fe3 100644 --- a/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx +++ b/src/legacy/core_plugins/data/public/filter/filter_bar/filter_item.tsx @@ -28,7 +28,7 @@ import { import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { Component } from 'react'; -import { IndexPattern } from 'ui/index_patterns'; +import { IndexPattern } from '../../index_patterns'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index.ts b/src/legacy/core_plugins/data/public/index_patterns/index.ts index c7ff48407ff7c..471c646ff7671 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index.ts @@ -26,4 +26,6 @@ export { StaticIndexPattern, StaticIndexPatternField, Field, + fixtures, + utils, } from './index_patterns_service'; diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts index 8210ed8aad5f7..518a4b8c6c140 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns_service.ts @@ -31,6 +31,8 @@ import { IndexPatterns } from 'ui/index_patterns/index'; // @ts-ignore import { validateIndexPattern } from 'ui/index_patterns/index'; +import { isFilterable, getFromSavedObject } from 'ui/index_patterns/static_utils'; + // IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field import * as types from 'ui/index_patterns'; @@ -75,7 +77,12 @@ const ui = { IndexPatternSelect, }; -export { validateIndexPattern, constants, fixtures, ui, IndexPatterns }; +const utils = { + getFromSavedObject, + isFilterable, +}; + +export { validateIndexPattern, constants, fixtures, ui, IndexPatterns, utils }; /** @public */ export type IndexPatternsSetup = ReturnType; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx index 1256ef5dc5104..28a31610a40fb 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx +++ b/src/legacy/core_plugins/data/public/query/query_bar/components/query_bar_input.tsx @@ -29,12 +29,12 @@ import { getAutocompleteProvider, } from 'ui/autocomplete_providers'; import { debounce, compact, isEqual, omit } from 'lodash'; -import { IndexPattern, StaticIndexPattern } from 'ui/index_patterns'; import { PersistedLog } from 'ui/persisted_log'; import chrome from 'ui/chrome'; import { kfetch } from 'ui/kfetch'; import { Storage } from 'ui/storage'; import { localStorage } from 'ui/storage/storage_service'; +import { IndexPattern, StaticIndexPattern } from '../../../index_patterns'; import { Query } from '../index'; import { fromUser, matchPairs, toUser } from '../lib'; import { QueryLanguageSwitcher } from './language_switcher'; diff --git a/src/legacy/core_plugins/data/public/query/query_bar/lib/fetch_index_patterns.ts b/src/legacy/core_plugins/data/public/query/query_bar/lib/fetch_index_patterns.ts index 091c7b74b6efa..3ba621a8f3d3f 100644 --- a/src/legacy/core_plugins/data/public/query/query_bar/lib/fetch_index_patterns.ts +++ b/src/legacy/core_plugins/data/public/query/query_bar/lib/fetch_index_patterns.ts @@ -18,8 +18,8 @@ */ import chrome from 'ui/chrome'; -import { getFromSavedObject } from 'ui/index_patterns/static_utils'; import { isEmpty } from 'lodash'; +import { utils as indexPatternUtils } from '../../../index_patterns'; const config = chrome.getUiSettingsClient(); @@ -45,7 +45,7 @@ export async function fetchIndexPatterns(indexPatternStrings: string[]) { ? exactMatches : [...exactMatches, await fetchDefaultIndexPattern()]; - return allMatches.map(getFromSavedObject); + return allMatches.map(indexPatternUtils.getFromSavedObject); } const fetchDefaultIndexPattern = async () => { From 6daf877b104941fa65f1e920882b79199464d211 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 24 Jul 2019 14:12:54 +0200 Subject: [PATCH 2/7] [SIEM] Timeline NOT working with unauthorized (#41767) (#41873) * Allow error to show in the application * Allow unauthorized user to use timeline api with redux storage * add callout to timeline to show + fix event details * Build fixes * fix pinned event * review I * fix details timeline test on api integration --- .../__snapshots__/event_details.test.tsx.snap | 80 -- .../__snapshots__/json_view.test.tsx.snap | 80 -- .../components/event_details/columns.tsx | 9 +- .../event_fields_browser.test.tsx | 8 +- .../event_details/event_fields_browser.tsx | 45 +- .../components/event_details/helpers.test.tsx | 8 +- .../public/components/event_details/types.ts | 10 + .../__snapshots__/timeline.test.tsx.snap | 1 + .../header/__snapshots__/index.test.tsx.snap | 1 + .../components/timeline/header/index.test.tsx | 29 + .../components/timeline/header/index.tsx | 14 + .../timeline/header/translations.ts | 15 + .../siem/public/components/timeline/index.tsx | 9 +- .../components/timeline/timeline.test.tsx | 12 + .../public/components/timeline/timeline.tsx | 3 + .../timeline/details/index.gql_query.ts | 4 - .../siem/public/graphql/introspection.json | 72 +- .../plugins/siem/public/graphql/types.ts | 24 +- .../plugins/siem/public/mock/global_state.ts | 1 + .../siem/public/mock/mock_detail_item.ts | 85 -- .../siem/public/store/timeline/actions.ts | 2 + .../siem/public/store/timeline/epic.ts | 7 + .../public/store/timeline/epic_favorite.ts | 9 +- .../siem/public/store/timeline/epic_note.ts | 8 +- .../store/timeline/epic_pinned_event.ts | 8 +- .../siem/public/store/timeline/helpers.ts | 1 + .../siem/public/store/timeline/reducer.ts | 6 + .../siem/public/store/timeline/selectors.ts | 9 + .../siem/public/store/timeline/types.ts | 1 + .../siem/server/graphql/events/schema.gql.ts | 4 - .../server/graphql/pinned_event/schema.gql.ts | 2 + .../server/graphql/timeline/schema.gql.ts | 2 + .../plugins/siem/server/graphql/types.ts | 72 +- .../lib/events/elasticsearch_adapter.ts | 77 +- .../plugins/siem/server/lib/events/mock.ts | 884 +++++++----------- .../siem/server/lib/note/saved_object.ts | 23 +- .../server/lib/pinned_event/saved_object.ts | 15 +- .../siem/server/lib/timeline/saved_object.ts | 158 ++-- .../apis/siem/timeline_details.ts | 518 ++-------- 39 files changed, 845 insertions(+), 1471 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/components/event_details/types.ts create mode 100644 x-pack/legacy/plugins/siem/public/components/timeline/header/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap index f69bc85d5ed4b..399892270f9fb 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -391,221 +391,141 @@ exports[`EventDetails rendering should match snapshot 1`] = ` data={ Array [ Object { - "category": "_id", - "description": "Each document has an _id that uniquely identifies it", - "example": "Y-6TfmcB0WOhS6qyMv3s", "field": "_id", "originalValue": "pEMaMmkBUV60JmNWmWVi", - "type": "keyword", "values": Array [ "pEMaMmkBUV60JmNWmWVi", ], }, Object { - "category": "_index", - "description": "An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.", - "example": "auditbeat-8.0.0-2019.02.19-000001", "field": "_index", "originalValue": "filebeat-8.0.0-2019.02.19-000001", - "type": "keyword", "values": Array [ "filebeat-8.0.0-2019.02.19-000001", ], }, Object { - "category": "_type", - "description": null, - "example": null, "field": "_type", "originalValue": "_doc", - "type": "keyword", "values": Array [ "_doc", ], }, Object { - "category": "_score", - "description": null, - "example": null, "field": "_score", "originalValue": 1, - "type": "long", "values": Array [ "1", ], }, Object { - "category": "@timestamp", - "description": "Date/time when the event originated.For log events this is the date/time when the event was generated, and not when it was read.Required field for all events.", - "example": "2016-05-23T08:05:34.853Z", "field": "@timestamp", "originalValue": "2019-02-28T16:50:54.621Z", - "type": "date", "values": Array [ "2019-02-28T16:50:54.621Z", ], }, Object { - "category": "agent", - "description": "Ephemeral identifier of this agent (if one exists).This id normally changes across restarts, but \`agent.id\` does not.", - "example": "8a4f500f", "field": "agent.ephemeral_id", "originalValue": "9d391ef2-a734-4787-8891-67031178c641", - "type": "keyword", "values": Array [ "9d391ef2-a734-4787-8891-67031178c641", ], }, Object { - "category": "agent", - "description": null, - "example": null, "field": "agent.hostname", "originalValue": "siem-kibana", - "type": "keyword", "values": Array [ "siem-kibana", ], }, Object { - "category": "agent", - "description": "Unique identifier of this agent (if one exists).Example: For Beats this would be beat.id.", - "example": "8a4f500d", "field": "agent.id", "originalValue": "5de03d5f-52f3-482e-91d4-853c7de073c3", - "type": "keyword", "values": Array [ "5de03d5f-52f3-482e-91d4-853c7de073c3", ], }, Object { - "category": "agent", - "description": "Type of the agent.The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.", - "example": "filebeat", "field": "agent.type", "originalValue": "filebeat", - "type": "keyword", "values": Array [ "filebeat", ], }, Object { - "category": "agent", - "description": "Version of the agent.", - "example": "6.0.0-rc2", "field": "agent.version", "originalValue": "8.0.0", - "type": "keyword", "values": Array [ "8.0.0", ], }, Object { - "category": "cloud", - "description": "Availability zone in which this host is running.", - "example": "us-east-1c", "field": "cloud.availability_zone", "originalValue": "projects/189716325846/zones/us-east1-b", - "type": "keyword", "values": Array [ "projects/189716325846/zones/us-east1-b", ], }, Object { - "category": "cloud", - "description": "Instance ID of the host machine.", - "example": "i-1234567890abcdef0", "field": "cloud.instance.id", "originalValue": "5412578377715150143", - "type": "keyword", "values": Array [ "5412578377715150143", ], }, Object { - "category": "cloud", - "description": "Instance name of the host machine.", - "example": null, "field": "cloud.instance.name", "originalValue": "siem-kibana", - "type": "keyword", "values": Array [ "siem-kibana", ], }, Object { - "category": "cloud", - "description": "Machine type of the host machine.", - "example": "t2.medium", "field": "cloud.machine.type", "originalValue": "projects/189716325846/machineTypes/n1-standard-1", - "type": "keyword", "values": Array [ "projects/189716325846/machineTypes/n1-standard-1", ], }, Object { - "category": "cloud", - "description": null, - "example": null, "field": "cloud.project.id", "originalValue": "elastic-beats", - "type": "keyword", "values": Array [ "elastic-beats", ], }, Object { - "category": "cloud", - "description": "Name of the cloud provider. Example values are ec2, gce, or digitalocean.", - "example": "ec2", "field": "cloud.provider", "originalValue": "gce", - "type": "keyword", "values": Array [ "gce", ], }, Object { - "category": "destination", - "description": "Bytes sent from the destination to the source.", - "example": "184", "field": "destination.bytes", "originalValue": 584, - "type": "long", "values": Array [ "584", ], }, Object { - "category": "destination", - "description": "IP address of the destination.Can be one or multiple IPv4 or IPv6 addresses.", - "example": null, "field": "destination.ip", "originalValue": "10.47.8.200", - "type": "ip", "values": Array [ "10.47.8.200", ], }, Object { - "category": "destination", - "description": "Packets sent from the destination to the source.", - "example": "12", "field": "destination.packets", "originalValue": 4, - "type": "long", "values": Array [ "4", ], }, Object { - "category": "destination", - "description": "Port of the destination.", - "example": null, "field": "destination.port", "originalValue": 902, - "type": "long", "values": Array [ "902", ], diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap index f26cc9d9000d4..a788b60afd6b3 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -5,221 +5,141 @@ exports[`JSON View rendering should match snapshot 1`] = ` data={ Array [ Object { - "category": "_id", - "description": "Each document has an _id that uniquely identifies it", - "example": "Y-6TfmcB0WOhS6qyMv3s", "field": "_id", "originalValue": "pEMaMmkBUV60JmNWmWVi", - "type": "keyword", "values": Array [ "pEMaMmkBUV60JmNWmWVi", ], }, Object { - "category": "_index", - "description": "An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.", - "example": "auditbeat-8.0.0-2019.02.19-000001", "field": "_index", "originalValue": "filebeat-8.0.0-2019.02.19-000001", - "type": "keyword", "values": Array [ "filebeat-8.0.0-2019.02.19-000001", ], }, Object { - "category": "_type", - "description": null, - "example": null, "field": "_type", "originalValue": "_doc", - "type": "keyword", "values": Array [ "_doc", ], }, Object { - "category": "_score", - "description": null, - "example": null, "field": "_score", "originalValue": 1, - "type": "long", "values": Array [ "1", ], }, Object { - "category": "@timestamp", - "description": "Date/time when the event originated.For log events this is the date/time when the event was generated, and not when it was read.Required field for all events.", - "example": "2016-05-23T08:05:34.853Z", "field": "@timestamp", "originalValue": "2019-02-28T16:50:54.621Z", - "type": "date", "values": Array [ "2019-02-28T16:50:54.621Z", ], }, Object { - "category": "agent", - "description": "Ephemeral identifier of this agent (if one exists).This id normally changes across restarts, but \`agent.id\` does not.", - "example": "8a4f500f", "field": "agent.ephemeral_id", "originalValue": "9d391ef2-a734-4787-8891-67031178c641", - "type": "keyword", "values": Array [ "9d391ef2-a734-4787-8891-67031178c641", ], }, Object { - "category": "agent", - "description": null, - "example": null, "field": "agent.hostname", "originalValue": "siem-kibana", - "type": "keyword", "values": Array [ "siem-kibana", ], }, Object { - "category": "agent", - "description": "Unique identifier of this agent (if one exists).Example: For Beats this would be beat.id.", - "example": "8a4f500d", "field": "agent.id", "originalValue": "5de03d5f-52f3-482e-91d4-853c7de073c3", - "type": "keyword", "values": Array [ "5de03d5f-52f3-482e-91d4-853c7de073c3", ], }, Object { - "category": "agent", - "description": "Type of the agent.The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.", - "example": "filebeat", "field": "agent.type", "originalValue": "filebeat", - "type": "keyword", "values": Array [ "filebeat", ], }, Object { - "category": "agent", - "description": "Version of the agent.", - "example": "6.0.0-rc2", "field": "agent.version", "originalValue": "8.0.0", - "type": "keyword", "values": Array [ "8.0.0", ], }, Object { - "category": "cloud", - "description": "Availability zone in which this host is running.", - "example": "us-east-1c", "field": "cloud.availability_zone", "originalValue": "projects/189716325846/zones/us-east1-b", - "type": "keyword", "values": Array [ "projects/189716325846/zones/us-east1-b", ], }, Object { - "category": "cloud", - "description": "Instance ID of the host machine.", - "example": "i-1234567890abcdef0", "field": "cloud.instance.id", "originalValue": "5412578377715150143", - "type": "keyword", "values": Array [ "5412578377715150143", ], }, Object { - "category": "cloud", - "description": "Instance name of the host machine.", - "example": null, "field": "cloud.instance.name", "originalValue": "siem-kibana", - "type": "keyword", "values": Array [ "siem-kibana", ], }, Object { - "category": "cloud", - "description": "Machine type of the host machine.", - "example": "t2.medium", "field": "cloud.machine.type", "originalValue": "projects/189716325846/machineTypes/n1-standard-1", - "type": "keyword", "values": Array [ "projects/189716325846/machineTypes/n1-standard-1", ], }, Object { - "category": "cloud", - "description": null, - "example": null, "field": "cloud.project.id", "originalValue": "elastic-beats", - "type": "keyword", "values": Array [ "elastic-beats", ], }, Object { - "category": "cloud", - "description": "Name of the cloud provider. Example values are ec2, gce, or digitalocean.", - "example": "ec2", "field": "cloud.provider", "originalValue": "gce", - "type": "keyword", "values": Array [ "gce", ], }, Object { - "category": "destination", - "description": "Bytes sent from the destination to the source.", - "example": "184", "field": "destination.bytes", "originalValue": 584, - "type": "long", "values": Array [ "584", ], }, Object { - "category": "destination", - "description": "IP address of the destination.Can be one or multiple IPv4 or IPv6 addresses.", - "example": null, "field": "destination.ip", "originalValue": "10.47.8.200", - "type": "ip", "values": Array [ "10.47.8.200", ], }, Object { - "category": "destination", - "description": "Packets sent from the destination to the source.", - "example": "12", "field": "destination.packets", "originalValue": 4, - "type": "long", "values": Array [ "4", ], }, Object { - "category": "destination", - "description": "Port of the destination.", - "example": null, "field": "destination.port", "originalValue": 902, - "type": "long", "values": Array [ "902", ], diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx index e0accfc964337..b23fcc4c1cdf3 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/columns.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; import { DragEffects } from '../drag_and_drop/draggable_wrapper'; import { DefaultDraggable } from '../draggables'; -import { DetailItem, ToStringArray } from '../../graphql/types'; +import { ToStringArray } from '../../graphql/types'; import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; import { DraggableFieldBadge } from '../draggables/field_badge'; import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; @@ -28,6 +28,7 @@ import * as i18n from './translations'; import { OverflowField } from '../tables/helpers'; import { DATE_FIELD_TYPE, MESSAGE_FIELD_NAME } from '../timeline/body/renderers/constants'; import { EVENT_DURATION_FIELD_NAME } from '../duration'; +import { EventFieldsData } from './types'; const HoverActionsContainer = styled(EuiPanel)` align-items: center; @@ -76,7 +77,7 @@ export const getColumns = ({ name: i18n.FIELD, sortable: true, truncateText: false, - render: (field: string, data: DetailItem) => ( + render: (field: string, data: EventFieldsData) => ( ( + render: (values: ToStringArray | null | undefined, data: EventFieldsData) => ( {values != null && values.map((value, i) => ( @@ -174,7 +175,7 @@ export const getColumns = ({ { field: 'description', name: i18n.DESCRIPTION, - render: (description: string | null | undefined, data: DetailItem) => ( + render: (description: string | null | undefined, data: EventFieldsData) => ( {`${description || ''} ${getExampleText(data.example)}`} ), sortable: true, diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx index 2a38e0816dd12..f481da69232b2 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx @@ -101,7 +101,7 @@ describe('EventFieldsBrowser', () => { .find('[data-test-subj="field-name"]') .at(0) .text() - ).toEqual('_id'); + ).toEqual('@timestamp'); }); }); @@ -124,7 +124,7 @@ describe('EventFieldsBrowser', () => { .find('[data-test-subj="draggable-content"]') .at(0) .text() - ).toEqual('pEMaMmkBUV60JmNWmWVi'); + ).toEqual('Feb 28, 2019 @ 16:50:54.621'); }); }); @@ -149,7 +149,9 @@ describe('EventFieldsBrowser', () => { .find('.euiTableRowCell') .at(3) .text() - ).toContain('Each document has an _id that uniquely identifies it'); + ).toContain( + 'DescriptionDate/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events. Example: 2016-05-23T08:05:34.853Z' + ); }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx index c10cb09d51ca1..e8be940fe831d 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.tsx @@ -11,7 +11,8 @@ import { import * as React from 'react'; import { pure } from 'recompose'; -import { BrowserFields } from '../../containers/source'; +import { sortBy } from 'lodash'; +import { BrowserFields, getAllFieldsByName } from '../../containers/source'; import { DetailItem } from '../../graphql/types'; import { OnUpdateColumns } from '../timeline/events'; @@ -29,22 +30,28 @@ interface Props { /** Renders a table view or JSON view of the `ECS` `data` */ export const EventFieldsBrowser = pure( - ({ browserFields, data, eventId, isLoading, onUpdateColumns, timelineId }) => ( - ({ - ...item, - valuesConcatenated: item.values != null ? item.values.join() : '', - }))} - columns={getColumns({ - browserFields, - eventId, - isLoading, - onUpdateColumns, - timelineId, - })} - pagination={false} - search={search} - sorting={true} - /> - ) + ({ browserFields, data, eventId, isLoading, onUpdateColumns, timelineId }) => { + const fieldsByName = getAllFieldsByName(browserFields); + return ( + { + return { + ...item, + ...fieldsByName[item.field], + valuesConcatenated: item.values != null ? item.values.join() : '', + }; + })} + columns={getColumns({ + browserFields, + eventId, + isLoading, + onUpdateColumns, + timelineId, + })} + pagination={false} + search={search} + sorting={true} + /> + ); + } ); diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/helpers.test.tsx index 2104a331198dd..e662283f8b000 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/helpers.test.tsx @@ -7,13 +7,17 @@ import { mockDetailItemData } from '../../mock/mock_detail_item'; import { getExampleText, getIconFromType } from './helpers'; +import { mockBrowserFields } from '../../containers/source/mock'; -const aField = mockDetailItemData[0]; +const aField = { + ...mockDetailItemData[4], + ...mockBrowserFields.base.fields!['@timestamp'], +}; describe('helpers', () => { describe('getExampleText', () => { test('it returns the expected example text when the field contains an example', () => { - expect(getExampleText(aField.example)).toEqual('Example: Y-6TfmcB0WOhS6qyMv3s'); + expect(getExampleText(aField.example)).toEqual('Example: 2016-05-23T08:05:34.853Z'); }); test(`it returns an empty string when the field's example is an empty string`, () => { diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/types.ts b/x-pack/legacy/plugins/siem/public/components/event_details/types.ts new file mode 100644 index 0000000000000..4e351fcdf98e4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/event_details/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BrowserField } from '../../containers/source'; +import { DetailItem } from '../../graphql/types'; + +export type EventFieldsData = BrowserField & DetailItem; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap index 56854111e5e5f..05fb08a6bf6ec 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap @@ -741,6 +741,7 @@ In other use cases the message field can be used to concatenate different values onToggleDataProviderEnabled={[MockFunction]} onToggleDataProviderExcluded={[MockFunction]} show={true} + showCallOutUnauthorizedMsg={false} sort={ Object { "columnId": "@timestamp", diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap index e2ae412ffa8e3..332942c3e3a86 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap @@ -230,6 +230,7 @@ exports[`Header rendering renders correctly against snapshot 1`] = ` onToggleDataProviderEnabled={[MockFunction]} onToggleDataProviderExcluded={[MockFunction]} show={true} + showCallOutUnauthorizedMsg={false} sort={ Object { "columnId": "@timestamp", diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx index 539954ba7cb4b..baabd2e0f6a82 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx @@ -33,6 +33,7 @@ describe('Header', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} sort={{ columnId: '@timestamp', sortDirection: Direction.desc, @@ -57,6 +58,7 @@ describe('Header', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} sort={{ columnId: '@timestamp', sortDirection: Direction.desc, @@ -67,5 +69,32 @@ describe('Header', () => { expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true); }); + + test('it renders the unauthorized call out providers', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').exists()).toEqual(true); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx index 15624dec9b7e7..ca870304a7fb8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiCallOut } from '@elastic/eui'; import * as React from 'react'; import { pure } from 'recompose'; import styled from 'styled-components'; @@ -23,6 +24,8 @@ import { import { StatefulSearchOrFilter } from '../search_or_filter'; import { BrowserFields } from '../../../containers/source'; +import * as i18n from './translations'; + interface Props { browserFields: BrowserFields; dataProviders: DataProvider[]; @@ -35,6 +38,7 @@ interface Props { onToggleDataProviderEnabled: OnToggleDataProviderEnabled; onToggleDataProviderExcluded: OnToggleDataProviderExcluded; show: boolean; + showCallOutUnauthorizedMsg: boolean; sort: Sort; } @@ -55,8 +59,18 @@ export const TimelineHeader = pure( onToggleDataProviderEnabled, onToggleDataProviderExcluded, show, + showCallOutUnauthorizedMsg, }) => ( + {showCallOutUnauthorizedMsg && ( + + )} { sort, start, show, + showCallOutUnauthorizedMsg, }: Props) => id !== this.props.id || flyoutHeaderHeight !== this.props.flyoutHeaderHeight || @@ -155,7 +157,8 @@ class StatefulTimelineComponent extends React.Component { pageCount !== this.props.pageCount || !isEqual(sort, this.props.sort) || start !== this.props.start || - show !== this.props.show; + show !== this.props.show || + showCallOutUnauthorizedMsg !== this.props.showCallOutUnauthorizedMsg; public componentDidMount() { const { createTimeline, id } = this.props; @@ -179,6 +182,7 @@ class StatefulTimelineComponent extends React.Component { kqlMode, kqlQueryExpression, show, + showCallOutUnauthorizedMsg, start, sort, } = this.props; @@ -208,6 +212,7 @@ class StatefulTimelineComponent extends React.Component { onToggleDataProviderEnabled={this.onToggleDataProviderEnabled} onToggleDataProviderExcluded={this.onToggleDataProviderExcluded} show={show!} + showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg} start={start} sort={sort!} /> @@ -274,6 +279,7 @@ class StatefulTimelineComponent extends React.Component { } const makeMapStateToProps = () => { + const getShowCallOutUnauthorizedMsg = timelineSelectors.getShowCallOutUnauthorizedMsg(); const getTimeline = timelineSelectors.getTimelineByIdSelector(); const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); const getInputsTimeline = inputsSelectors.getTimelineSelector(); @@ -303,6 +309,7 @@ const makeMapStateToProps = () => { kqlQueryExpression, sort, start: input.timerange.from, + showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state), show, }; }; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index c6c1356cac574..3dc223bbda41f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -67,6 +67,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -100,6 +101,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -136,6 +138,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -172,6 +175,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -213,6 +217,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -256,6 +261,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -307,6 +313,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={mockOnToggleDataProviderEnabled} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -362,6 +369,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={mockOnToggleDataProviderExcluded} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -420,6 +428,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -468,6 +477,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -522,6 +532,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={mockOnToggleDataProviderEnabled} onToggleDataProviderExcluded={jest.fn()} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> @@ -580,6 +591,7 @@ describe('Timeline', () => { onToggleDataProviderEnabled={jest.fn()} onToggleDataProviderExcluded={mockOnToggleDataProviderExcluded} show={true} + showCallOutUnauthorizedMsg={false} start={startDate} sort={sort} /> diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx index 51fb303376d01..006b84a1af9a3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.tsx @@ -71,6 +71,7 @@ interface Props { onToggleDataProviderEnabled: OnToggleDataProviderEnabled; onToggleDataProviderExcluded: OnToggleDataProviderExcluded; show: boolean; + showCallOutUnauthorizedMsg: boolean; start: number; sort: Sort; } @@ -99,6 +100,7 @@ export const Timeline = pure( onToggleDataProviderEnabled, onToggleDataProviderExcluded, show, + showCallOutUnauthorizedMsg, start, sort, }) => { @@ -134,6 +136,7 @@ export const Timeline = pure( onToggleDataProviderEnabled={onToggleDataProviderEnabled} onToggleDataProviderExcluded={onToggleDataProviderExcluded} show={show} + showCallOutUnauthorizedMsg={showCallOutUnauthorizedMsg} sort={sort} /> diff --git a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.gql_query.ts index 605f59cedfb65..4677d2328be87 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/details/index.gql_query.ts @@ -17,11 +17,7 @@ export const timelineDetailsQuery = gql` id TimelineDetails(eventId: $eventId, indexName: $indexName, defaultIndex: $defaultIndex) { data { - category - description - example field - type values originalValue } diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 3b86740c73dc7..baeba2e3415a7 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -527,6 +527,22 @@ "name": "PinnedEvent", "description": "", "fields": [ + { + "name": "code", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "pinnedEventId", "description": "", @@ -5451,34 +5467,6 @@ "name": "DetailItem", "description": "", "fields": [ - { - "name": "category", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "example", - "description": "", - "args": [], - "type": { "kind": "SCALAR", "name": "String", "ofType": null }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "field", "description": "", @@ -5491,18 +5479,6 @@ "isDeprecated": false, "deprecationReason": null }, - { - "name": "type", - "description": "", - "args": [], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { "kind": "SCALAR", "name": "String", "ofType": null } - }, - "isDeprecated": false, - "deprecationReason": null - }, { "name": "values", "description": "", @@ -9815,6 +9791,22 @@ "name": "ResponseFavoriteTimeline", "description": "", "fields": [ + { + "name": "code", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "Float", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "message", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "savedObjectId", "description": "", diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index 3246ef1ff49a3..bad3371bb2eed 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -79,6 +79,10 @@ export interface ResponseNotes { } export interface PinnedEvent { + code?: number | null; + + message?: string | null; + pinnedEventId: string; eventId?: string | null; @@ -846,16 +850,8 @@ export interface TimelineDetailsData { } export interface DetailItem { - category: string; - - description?: string | null; - - example?: string | null; - field: string; - type: string; - values?: ToStringArray | null; originalValue?: EsValue | null; @@ -1476,6 +1472,10 @@ export interface ResponseTimeline { } export interface ResponseFavoriteTimeline { + code?: number | null; + + message?: string | null; + savedObjectId: string; version: string; @@ -3733,16 +3733,8 @@ export namespace GetTimelineDetailsQuery { export type Data = { __typename?: 'DetailItem'; - category: string; - - description?: string | null; - - example?: string | null; - field: string; - type: string; - values?: ToStringArray | null; originalValue?: EsValue | null; diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts index e52d3d95dd1f0..b71742f2ad85e 100644 --- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts +++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts @@ -113,6 +113,7 @@ export const mockGlobalState: State = { }, dragAndDrop: { dataProviders: {} }, timeline: { + showCallOutUnauthorizedMsg: false, autoSavedWarningMsg: { timelineId: null, newTimelineModel: null, diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_detail_item.ts b/x-pack/legacy/plugins/siem/public/mock/mock_detail_item.ts index c44b44365a3ca..c25428649d563 100644 --- a/x-pack/legacy/plugins/siem/public/mock/mock_detail_item.ts +++ b/x-pack/legacy/plugins/siem/public/mock/mock_detail_item.ts @@ -10,187 +10,102 @@ export const mockDetailItemDataId = 'Y-6TfmcB0WOhS6qyMv3s'; export const mockDetailItemData: DetailItem[] = [ { - category: '_id', - description: 'Each document has an _id that uniquely identifies it', - example: 'Y-6TfmcB0WOhS6qyMv3s', field: '_id', - type: 'keyword', originalValue: 'pEMaMmkBUV60JmNWmWVi', values: ['pEMaMmkBUV60JmNWmWVi'], }, { - category: '_index', - description: - 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', - example: 'auditbeat-8.0.0-2019.02.19-000001', field: '_index', - type: 'keyword', originalValue: 'filebeat-8.0.0-2019.02.19-000001', values: ['filebeat-8.0.0-2019.02.19-000001'], }, { - category: '_type', - description: null, - example: null, field: '_type', - type: 'keyword', originalValue: '_doc', values: ['_doc'], }, { - category: '_score', - description: null, - example: null, field: '_score', - type: 'long', originalValue: 1, values: ['1'], }, { - category: '@timestamp', - description: - 'Date/time when the event originated.For log events this is the date/time when the event was generated, and not when it was read.Required field for all events.', - example: '2016-05-23T08:05:34.853Z', field: '@timestamp', - type: 'date', originalValue: '2019-02-28T16:50:54.621Z', values: ['2019-02-28T16:50:54.621Z'], }, { - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists).This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', field: 'agent.ephemeral_id', - type: 'keyword', originalValue: '9d391ef2-a734-4787-8891-67031178c641', values: ['9d391ef2-a734-4787-8891-67031178c641'], }, { - category: 'agent', - description: null, - example: null, field: 'agent.hostname', - type: 'keyword', originalValue: 'siem-kibana', values: ['siem-kibana'], }, { - category: 'agent', - description: - 'Unique identifier of this agent (if one exists).Example: For Beats this would be beat.id.', - example: '8a4f500d', field: 'agent.id', - type: 'keyword', originalValue: '5de03d5f-52f3-482e-91d4-853c7de073c3', values: ['5de03d5f-52f3-482e-91d4-853c7de073c3'], }, { - category: 'agent', - description: - 'Type of the agent.The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', - example: 'filebeat', field: 'agent.type', - type: 'keyword', originalValue: 'filebeat', values: ['filebeat'], }, { - category: 'agent', - description: 'Version of the agent.', - example: '6.0.0-rc2', field: 'agent.version', - type: 'keyword', originalValue: '8.0.0', values: ['8.0.0'], }, { - category: 'cloud', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', field: 'cloud.availability_zone', - type: 'keyword', originalValue: 'projects/189716325846/zones/us-east1-b', values: ['projects/189716325846/zones/us-east1-b'], }, { - category: 'cloud', - description: 'Instance ID of the host machine.', - example: 'i-1234567890abcdef0', field: 'cloud.instance.id', - type: 'keyword', originalValue: '5412578377715150143', values: ['5412578377715150143'], }, { - category: 'cloud', - description: 'Instance name of the host machine.', - example: null, field: 'cloud.instance.name', - type: 'keyword', originalValue: 'siem-kibana', values: ['siem-kibana'], }, { - category: 'cloud', - description: 'Machine type of the host machine.', - example: 't2.medium', field: 'cloud.machine.type', - type: 'keyword', originalValue: 'projects/189716325846/machineTypes/n1-standard-1', values: ['projects/189716325846/machineTypes/n1-standard-1'], }, { - category: 'cloud', - description: null, - example: null, field: 'cloud.project.id', - type: 'keyword', originalValue: 'elastic-beats', values: ['elastic-beats'], }, { - category: 'cloud', - description: 'Name of the cloud provider. Example values are ec2, gce, or digitalocean.', - example: 'ec2', field: 'cloud.provider', - type: 'keyword', originalValue: 'gce', values: ['gce'], }, { - category: 'destination', - description: 'Bytes sent from the destination to the source.', - example: '184', field: 'destination.bytes', - type: 'long', originalValue: 584, values: ['584'], }, { - category: 'destination', - description: 'IP address of the destination.Can be one or multiple IPv4 or IPv6 addresses.', - example: null, field: 'destination.ip', - type: 'ip', originalValue: '10.47.8.200', values: ['10.47.8.200'], }, { - category: 'destination', - description: 'Packets sent from the destination to the source.', - example: '12', field: 'destination.packets', - type: 'long', originalValue: 4, values: ['4'], }, { - category: 'destination', - description: 'Port of the destination.', - example: null, field: 'destination.port', - type: 'long', originalValue: 902, values: ['902'], }, diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts index 00977c959282a..6c36a5fae6e89 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/actions.ts @@ -184,3 +184,5 @@ export const updateAutoSaveMsg = actionCreator<{ timelineId: string | null; newTimelineModel: TimelineModel | null; }>('UPDATE_AUTO_SAVE'); + +export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG'); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts index c65f91c99a80d..2b31ac9505d18 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic.ts @@ -29,6 +29,7 @@ import { TimelineResult, } from '../../graphql/types'; import { AppApolloClient } from '../../lib/lib'; +import { addError } from '../app/actions'; import { NotesById } from '../app/model'; import { TimeRange } from '../inputs/model'; @@ -55,6 +56,7 @@ import { endTimelineSaving, createTimeline, addTimeline, + showCallOutUnauthorizedMsg, } from './actions'; import { TimelineModel } from './model'; import { epicPersistNote, timelineNoteActionsType } from './epic_note'; @@ -131,6 +133,9 @@ export const createTimelineEpic = (): Epic< withLatestFrom(timeline$), filter(([action, timeline]) => { const timelineId: TimelineModel = timeline[get('payload.id', action)]; + if (action.type === addError.type) { + return true; + } if (action.type === createTimeline.type) { myEpicTimelineId.setTimelineId(null); myEpicTimelineId.setTimelineVersion(null); @@ -186,6 +191,7 @@ export const createTimelineEpic = (): Epic< mergeMap(([result, recentTimeline]) => { const savedTimeline = recentTimeline[get('payload.id', action)]; const response: ResponseTimeline = get('data.persistTimeline', result); + const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; return [ response.code === 409 @@ -202,6 +208,7 @@ export const createTimelineEpic = (): Epic< isSaving: false, }, }), + ...callOutMsg, endTimelineSaving({ id: get('payload.id', action), }), diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts index 9e8201073737a..62b54b959f215 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_favorite.ts @@ -14,12 +14,13 @@ import { filter, mergeMap, withLatestFrom, startWith, takeUntil } from 'rxjs/ope import { persistTimelineFavoriteMutation } from '../../containers/timeline/favorite/persist.gql_query'; import { PersistTimelineFavoriteMutation, ResponseFavoriteTimeline } from '../../graphql/types'; - +import { addError } from '../app/actions'; import { endTimelineSaving, updateIsFavorite, updateTimeline, startTimelineSaving, + showCallOutUnauthorizedMsg, } from './actions'; import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; import { refetchQueries } from './refetch_queries'; @@ -53,7 +54,10 @@ export const epicPersistTimelineFavorite = ( mergeMap(([result, recentTimelines]) => { const savedTimeline = recentTimelines[get('payload.id', action)]; const response: ResponseFavoriteTimeline = get('data.persistFavorite', result); + const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + return [ + ...callOutMsg, updateTimeline({ id: get('payload.id', action), timeline: { @@ -73,6 +77,9 @@ export const epicPersistTimelineFavorite = ( action$.pipe( withLatestFrom(timeline$), filter(([checkAction, updatedTimeline]) => { + if (checkAction.type === addError.type) { + return true; + } if ( checkAction.type === endTimelineSaving.type && updatedTimeline[get('payload.id', checkAction)].savedObjectId != null diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts index 2e47e0e4ab7ec..866617281c927 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_note.ts @@ -14,7 +14,7 @@ import { filter, mergeMap, switchMap, withLatestFrom, startWith, takeUntil } fro import { persistTimelineNoteMutation } from '../../containers/timeline/notes/persist.gql_query'; import { PersistTimelineNoteMutation, ResponseNote } from '../../graphql/types'; -import { updateNote } from '../app/actions'; +import { updateNote, addError } from '../app/actions'; import { NotesById } from '../app/model'; import { @@ -23,6 +23,7 @@ import { endTimelineSaving, updateTimeline, startTimelineSaving, + showCallOutUnauthorizedMsg, } from './actions'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { refetchQueries } from './refetch_queries'; @@ -63,8 +64,10 @@ export const epicPersistNote = ( mergeMap(([result, recentTimeline, recentNotes]) => { const noteIdRedux = get('payload.noteId', action); const response: ResponseNote = get('data.persistNote', result); + const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; return [ + ...callOutMsg, recentTimeline[get('payload.id', action)].savedObjectId == null ? updateTimeline({ id: get('payload.id', action), @@ -100,6 +103,9 @@ export const epicPersistNote = ( action$.pipe( withLatestFrom(timeline$), filter(([checkAction, updatedTimeline]) => { + if (checkAction.type === addError.type) { + return true; + } if ( checkAction.type === endTimelineSaving.type && updatedTimeline[get('payload.id', checkAction)].savedObjectId != null diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts b/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts index 05451de91983b..8202c2f0dc49b 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/epic_pinned_event.ts @@ -14,13 +14,14 @@ import { filter, mergeMap, startWith, withLatestFrom, takeUntil } from 'rxjs/ope import { persistTimelinePinnedEventMutation } from '../../containers/timeline/pinned_event/persist.gql_query'; import { PersistTimelinePinnedEventMutation, PinnedEvent } from '../../graphql/types'; - +import { addError } from '../app/actions'; import { pinEvent, endTimelineSaving, unPinEvent, updateTimeline, startTimelineSaving, + showCallOutUnauthorizedMsg, } from './actions'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { refetchQueries } from './refetch_queries'; @@ -63,6 +64,7 @@ export const epicPersistPinnedEvent = ( mergeMap(([result, recentTimeline]) => { const savedTimeline = recentTimeline[get('payload.id', action)]; const response: PinnedEvent = get('data.persistPinnedEventOnTimeline', result); + const callOutMsg = response && response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; return [ response != null @@ -94,6 +96,7 @@ export const epicPersistPinnedEvent = ( ), }, }), + ...callOutMsg, endTimelineSaving({ id: get('payload.id', action), }), @@ -104,6 +107,9 @@ export const epicPersistPinnedEvent = ( action$.pipe( withLatestFrom(timeline$), filter(([checkAction, updatedTimeline]) => { + if (checkAction.type === addError.type) { + return true; + } if ( checkAction.type === endTimelineSaving.type && updatedTimeline[get('payload.id', checkAction)].savedObjectId != null diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts index 7fc2c2497b4a1..34759730d2563 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/helpers.ts @@ -29,6 +29,7 @@ export const initialTimelineState: TimelineState = { timelineId: null, newTimelineModel: null, }, + showCallOutUnauthorizedMsg: false, }; interface AddTimelineHistoryParams { diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/reducer.ts b/x-pack/legacy/plugins/siem/public/store/timeline/reducer.ts index 44615cb288396..7809245b9bb66 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/reducer.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/reducer.ts @@ -21,6 +21,7 @@ import { removeColumn, removeProvider, setKqlFilterQueryDraft, + showCallOutUnauthorizedMsg, showTimeline, startTimelineSaving, unPinEvent, @@ -89,6 +90,7 @@ export const initialTimelineState: TimelineState = { timelineId: null, newTimelineModel: null, }, + showCallOutUnauthorizedMsg: false, }; /** The reducer for all timeline actions */ @@ -348,4 +350,8 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) newTimelineModel, }, })) + .case(showCallOutUnauthorizedMsg, state => ({ + ...state, + showCallOutUnauthorizedMsg: true, + })) .build(); diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts b/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts index 7890348aac4a7..8c6da2d356aa7 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/selectors.ts @@ -16,6 +16,9 @@ const selectTimelineById = (state: State): TimelineById => state.timeline.timeli const selectAutoSaveMsg = (state: State): AutoSavedWarningMsg => state.timeline.autoSavedWarningMsg; +const selectCallOutUnauthorizedMsg = (state: State): boolean => + state.timeline.showCallOutUnauthorizedMsg; + export const selectTimeline = (state: State, timelineId: string): TimelineModel => state.timeline.timelineById[timelineId]; @@ -29,6 +32,12 @@ export const timelineByIdSelector = createSelector( timelineById => timelineById ); +export const getShowCallOutUnauthorizedMsg = () => + createSelector( + selectCallOutUnauthorizedMsg, + showCallOutUnauthorizedMsg => showCallOutUnauthorizedMsg + ); + export const getTimelineByIdSelector = () => createSelector( selectTimeline, diff --git a/x-pack/legacy/plugins/siem/public/store/timeline/types.ts b/x-pack/legacy/plugins/siem/public/store/timeline/types.ts index 19cb7e8560a01..5395df6941973 100644 --- a/x-pack/legacy/plugins/siem/public/store/timeline/types.ts +++ b/x-pack/legacy/plugins/siem/public/store/timeline/types.ts @@ -22,4 +22,5 @@ export const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference export interface TimelineState { timelineById: TimelineById; autoSavedWarningMsg: AutoSavedWarningMsg; + showCallOutUnauthorizedMsg: boolean; } diff --git a/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts index f934faffcf4f9..cddd8a11f55fb 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/events/schema.gql.ts @@ -41,11 +41,7 @@ export const eventsSchema = gql` } type DetailItem { - category: String! - description: String - example: String field: String! - type: String! values: ToStringArray originalValue: EsValue } diff --git a/x-pack/legacy/plugins/siem/server/graphql/pinned_event/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/pinned_event/schema.gql.ts index 7bd5d95a2b06f..a797cd6720af2 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/pinned_event/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/pinned_event/schema.gql.ts @@ -12,6 +12,8 @@ export const pinnedEventSchema = gql` ######################### type PinnedEvent { + code: Float + message: String pinnedEventId: ID! eventId: ID timelineId: ID diff --git a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts index eb465fab5f18a..a417ecb82b2d2 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/timeline/schema.gql.ts @@ -201,6 +201,8 @@ export const timelineSchema = gql` } type ResponseFavoriteTimeline { + code: Float + message: String savedObjectId: String! version: String! favorite: [FavoriteTimelineResult!] diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index 1fd83d65b2621..27116af2d20a4 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -108,6 +108,10 @@ export interface ResponseNotes { } export interface PinnedEvent { + code?: number | null; + + message?: string | null; + pinnedEventId: string; eventId?: string | null; @@ -875,16 +879,8 @@ export interface TimelineDetailsData { } export interface DetailItem { - category: string; - - description?: string | null; - - example?: string | null; - field: string; - type: string; - values?: ToStringArray | null; originalValue?: EsValue | null; @@ -1505,6 +1501,10 @@ export interface ResponseTimeline { } export interface ResponseFavoriteTimeline { + code?: number | null; + + message?: string | null; + savedObjectId: string; version: string; @@ -2381,6 +2381,10 @@ export namespace ResponseNotesResolvers { export namespace PinnedEventResolvers { export interface Resolvers { + code?: CodeResolver; + + message?: MessageResolver; + pinnedEventId?: PinnedEventIdResolver; eventId?: EventIdResolver; @@ -2400,6 +2404,16 @@ export namespace PinnedEventResolvers { version?: VersionResolver; } + export type CodeResolver< + R = number | null, + Parent = PinnedEvent, + Context = SiemContext + > = Resolver; + export type MessageResolver< + R = string | null, + Parent = PinnedEvent, + Context = SiemContext + > = Resolver; export type PinnedEventIdResolver< R = string, Parent = PinnedEvent, @@ -5194,46 +5208,18 @@ export namespace TimelineDetailsDataResolvers { export namespace DetailItemResolvers { export interface Resolvers { - category?: CategoryResolver; - - description?: DescriptionResolver; - - example?: ExampleResolver; - field?: FieldResolver; - type?: TypeResolver; - values?: ValuesResolver; originalValue?: OriginalValueResolver; } - export type CategoryResolver = Resolver< - R, - Parent, - Context - >; - export type DescriptionResolver< - R = string | null, - Parent = DetailItem, - Context = SiemContext - > = Resolver; - export type ExampleResolver< - R = string | null, - Parent = DetailItem, - Context = SiemContext - > = Resolver; export type FieldResolver = Resolver< R, Parent, Context >; - export type TypeResolver = Resolver< - R, - Parent, - Context - >; export type ValuesResolver< R = ToStringArray | null, Parent = DetailItem, @@ -7386,6 +7372,10 @@ export namespace ResponseTimelineResolvers { export namespace ResponseFavoriteTimelineResolvers { export interface Resolvers { + code?: CodeResolver; + + message?: MessageResolver; + savedObjectId?: SavedObjectIdResolver; version?: VersionResolver; @@ -7393,6 +7383,16 @@ export namespace ResponseFavoriteTimelineResolvers { favorite?: FavoriteResolver; } + export type CodeResolver< + R = number | null, + Parent = ResponseFavoriteTimeline, + Context = SiemContext + > = Resolver; + export type MessageResolver< + R = string | null, + Parent = ResponseFavoriteTimeline, + Context = SiemContext + > = Resolver; export type SavedObjectIdResolver< R = string, Parent = ResponseFavoriteTimeline, diff --git a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts index 4a6bd18afa1ee..faf359d75d106 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/elasticsearch_adapter.ts @@ -27,17 +27,11 @@ import { TimelineDetailsData, TimelineEdges, } from '../../graphql/types'; -import { getDocumentation, getIndexAlias, hasDocumentation } from '../../utils/beat_schema'; import { baseCategoryFields } from '../../utils/beat_schema/8.0.0'; import { reduceFields } from '../../utils/build_query/reduce_fields'; import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query'; import { eventFieldsMap } from '../ecs_fields'; -import { - FrameworkAdapter, - FrameworkRequest, - MappingProperties, - RequestOptions, -} from '../framework'; +import { FrameworkAdapter, FrameworkRequest, RequestOptions } from '../framework'; import { TermAggregation } from '../types'; import { buildDetailsQuery, buildQuery } from './query.dsl'; @@ -126,14 +120,11 @@ export class ElasticsearchEventsAdapter implements EventsAdapter { options: RequestDetailsOptions ): Promise { const dsl = buildDetailsQuery(options.indexName, options.eventId); - const [mapResponse, searchResponse] = await Promise.all([ - this.framework.callWithRequest(request, 'indices.getMapping', { - allowNoIndices: true, - ignoreUnavailable: true, - index: options.indexName, - }), - this.framework.callWithRequest(request, 'search', dsl), - ]); + const searchResponse = await this.framework.callWithRequest( + request, + 'search', + dsl + ); const sourceData = getOr({}, 'hits.hits.0._source', searchResponse); const hitsData = getOr({}, 'hits.hits.0', searchResponse); @@ -144,14 +135,7 @@ export class ElasticsearchEventsAdapter implements EventsAdapter { }; return { - data: getSchemaFromData( - { - ...addBasicElasticSearchProperties(), - ...getOr({}, [options.indexName, 'mappings', 'properties'], mapResponse), - }, - getDataFromHits(merge(sourceData, hitsData)), - getIndexAlias(options.defaultIndex, options.indexName) - ), + data: getDataFromHits(merge(sourceData, hitsData)), inspect, }; } @@ -307,50 +291,3 @@ const getDataFromHits = (sources: EventSource, category?: string, path?: string) } return accumulator; }, []); - -const getSchemaFromData = ( - properties: MappingProperties, - data: DetailItem[], - index: string, - path?: string -): DetailItem[] => - !isEmpty(properties) - ? Object.keys(properties).reduce((accumulator, property) => { - const item = get(property, properties); - const field = path ? `${path}.${property}` : property; - const dataFilterItem = data.filter(dataItem => dataItem.field === field); - if (item.properties == null && dataFilterItem.length === 1) { - const dataItem = dataFilterItem[0]; - const dataFromMapping = { - type: get([property, 'type'], properties), - }; - return [ - ...accumulator, - { - ...dataItem, - ...(hasDocumentation(index, field) - ? merge(getDocumentation(index, field), dataFromMapping) - : dataFromMapping), - }, - ]; - } else if (item.properties != null) { - return [...accumulator, ...getSchemaFromData(item.properties, data, index, field)]; - } - return accumulator; - }, []) - : data; - -const addBasicElasticSearchProperties = () => ({ - _id: { - type: 'keyword', - }, - _index: { - type: 'keyword', - }, - _type: { - type: 'keyword', - }, - _score: { - type: 'long', - }, -}); diff --git a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts b/x-pack/legacy/plugins/siem/server/lib/events/mock.ts index 3ee0667be61cb..b6b6a1f9c2934 100644 --- a/x-pack/legacy/plugins/siem/server/lib/events/mock.ts +++ b/x-pack/legacy/plugins/siem/server/lib/events/mock.ts @@ -2883,695 +2883,529 @@ export const mockTimelineDetailsResult = { response: [JSON.stringify(mockTimelineDetailsInspectResponse, null, 2)], }, data: [ - { - category: '_id', - field: '_id', - values: 'TUfUymkBCQofM5eXGBYL', - originalValue: 'TUfUymkBCQofM5eXGBYL', - description: 'Each document has an _id that uniquely identifies it', - example: 'Y-6TfmcB0WOhS6qyMv3s', - footnote: '', - group: 1, - level: 'core', - name: '_id', - required: true, - type: 'keyword', - }, - { - category: '_index', - field: '_index', - values: 'auditbeat-8.0.0-2019.03.29-000003', - originalValue: 'auditbeat-8.0.0-2019.03.29-000003', - description: - 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', - example: 'auditbeat-8.0.0-2019.02.19-000001', - footnote: '', - group: 1, - level: 'core', - name: '_index', - required: true, - type: 'keyword', - }, - { - category: '_type', - field: '_type', - values: '_doc', - originalValue: '_doc', - type: 'keyword', - }, - { - category: '_score', - field: '_score', - values: 1, - originalValue: 1, - type: 'long', - }, { category: '@timestamp', field: '@timestamp', values: '2019-03-29T19:01:23.420Z', originalValue: '2019-03-29T19:01:23.420Z', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - name: '@timestamp', - type: 'date', - }, - { - category: 'agent', - field: 'agent.ephemeral_id', - values: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', - originalValue: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - name: 'ephemeral_id', - type: 'keyword', - }, - { - category: 'agent', - field: 'agent.hostname', - values: 'zeek-london', - originalValue: 'zeek-london', - type: 'keyword', }, { - category: 'agent', - field: 'agent.id', - values: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', - originalValue: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - name: 'id', - type: 'keyword', + category: 'service', + field: 'service.type', + values: 'auditd', + originalValue: 'auditd', }, { - category: 'agent', - field: 'agent.type', - values: 'auditbeat', - originalValue: 'auditbeat', - description: - 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', - example: 'filebeat', - name: 'type', - type: 'keyword', + category: 'user', + field: 'user.audit.id', + values: 'unset', + originalValue: 'unset', }, { - category: 'agent', - field: 'agent.version', - values: '8.0.0', - originalValue: '8.0.0', - description: 'Version of the agent.', - example: '6.0.0-rc2', - name: 'version', - type: 'keyword', + category: 'user', + field: 'user.group.id', + values: '0', + originalValue: '0', }, { - category: 'auditd', - field: 'auditd.data.a0', - values: 'ffffff9c', - originalValue: 'ffffff9c', - type: 'keyword', + category: 'user', + field: 'user.group.name', + values: 'root', + originalValue: 'root', }, { - category: 'auditd', - field: 'auditd.data.a1', - values: '7fe0f63df220', - originalValue: '7fe0f63df220', - type: 'keyword', + category: 'user', + field: 'user.effective.group.id', + values: '0', + originalValue: '0', }, { - category: 'auditd', - field: 'auditd.data.a2', - values: '80000', - originalValue: '80000', - type: 'keyword', + category: 'user', + field: 'user.effective.group.name', + values: 'root', + originalValue: 'root', }, { - category: 'auditd', - field: 'auditd.data.a3', + category: 'user', + field: 'user.effective.id', values: '0', originalValue: '0', - type: 'keyword', }, { - category: 'auditd', - field: 'auditd.data.arch', - values: 'x86_64', - originalValue: 'x86_64', - type: 'keyword', + category: 'user', + field: 'user.effective.name', + values: 'root', + originalValue: 'root', }, { - category: 'auditd', - field: 'auditd.data.exit', - values: '12', - originalValue: '12', - type: 'keyword', + category: 'user', + field: 'user.filesystem.group.name', + values: 'root', + originalValue: 'root', }, { - category: 'auditd', - field: 'auditd.data.syscall', - values: 'openat', - originalValue: 'openat', - type: 'keyword', + category: 'user', + field: 'user.filesystem.group.id', + values: '0', + originalValue: '0', }, { - category: 'auditd', - field: 'auditd.data.tty', - values: '(none)', - originalValue: '(none)', - type: 'keyword', + category: 'user', + field: 'user.filesystem.name', + values: 'root', + originalValue: 'root', }, { - category: 'auditd', - field: 'auditd.message_type', - values: 'syscall', - originalValue: 'syscall', - type: 'keyword', + category: 'user', + field: 'user.filesystem.id', + values: '0', + originalValue: '0', }, { - category: 'auditd', - field: 'auditd.result', - values: 'success', - originalValue: 'success', - type: 'keyword', + category: 'user', + field: 'user.saved.group.id', + values: '0', + originalValue: '0', }, { - category: 'auditd', - field: 'auditd.sequence', - values: 8817905, - originalValue: 8817905, - type: 'long', + category: 'user', + field: 'user.saved.group.name', + values: 'root', + originalValue: 'root', }, { - category: 'auditd', - field: 'auditd.session', - values: 'unset', - originalValue: 'unset', - type: 'keyword', + category: 'user', + field: 'user.saved.id', + values: '0', + originalValue: '0', }, { - category: 'auditd', - field: 'auditd.summary.actor.primary', - values: 'unset', - originalValue: 'unset', - type: 'keyword', + category: 'user', + field: 'user.saved.name', + values: 'root', + originalValue: 'root', }, { - category: 'auditd', - field: 'auditd.summary.actor.secondary', + category: 'user', + field: 'user.id', + values: '0', + originalValue: '0', + }, + { + category: 'user', + field: 'user.name', values: 'root', originalValue: 'root', - type: 'keyword', }, { - category: 'auditd', - field: 'auditd.summary.how', + category: 'process', + field: 'process.executable', values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', - type: 'keyword', }, { - category: 'auditd', - field: 'auditd.summary.object.primary', - values: '/etc/passwd', - originalValue: '/etc/passwd', - type: 'keyword', + category: 'process', + field: 'process.working_directory', + values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', + originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', }, { - category: 'auditd', - field: 'auditd.summary.object.type', - values: 'file', - originalValue: 'file', - type: 'keyword', + category: 'process', + field: 'process.pid', + values: 15990, + originalValue: 15990, }, { - category: 'cloud', - field: 'cloud.instance.id', - values: '136398786', - originalValue: '136398786', - description: 'Instance ID of the host machine.', - example: 'i-1234567890abcdef0', - name: 'instance.id', - type: 'keyword', + category: 'process', + field: 'process.ppid', + values: 1, + originalValue: 1, }, { - category: 'cloud', - field: 'cloud.provider', - values: 'digitalocean', - originalValue: 'digitalocean', - description: 'Name of the cloud provider. Example values are ec2, gce, or digitalocean.', - example: 'ec2', - name: 'provider', - type: 'keyword', + category: 'process', + field: 'process.title', + values: + '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', + originalValue: + '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', }, { - category: 'cloud', - field: 'cloud.region', - values: 'lon1', - originalValue: 'lon1', - description: 'Region in which this host is running.', - example: 'us-east-1', - name: 'region', - type: 'keyword', + category: 'process', + field: 'process.name', + values: 'auditbeat', + originalValue: 'auditbeat', }, { - category: 'ecs', - field: 'ecs.version', - values: '1.0.0', - originalValue: '1.0.0', - description: - 'ECS version this event conforms to. `ecs.version` is a required field and must exist in all events. When querying across multiple indices -- which may conform to slightly different ECS versions -- this field lets integrations adjust to the schema version of the events. The current version is 1.0.0-beta2 .', - example: '1.0.0-beta2', - name: 'version', - type: 'keyword', + category: 'host', + field: 'host.architecture', + values: 'x86_64', + originalValue: 'x86_64', }, { - category: 'event', - field: 'event.action', - values: 'opened-file', - originalValue: 'opened-file', - description: - 'The action captured by the event. This describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.', - example: 'user-password-change', - name: 'action', - type: 'keyword', + category: 'host', + field: 'host.os.name', + values: 'Ubuntu', + originalValue: 'Ubuntu', }, { - category: 'event', - field: 'event.category', - values: 'audit-rule', - originalValue: 'audit-rule', - description: - 'Event category. This contains high-level information about the contents of the event. It is more generic than `event.action`, in the sense that typically a category contains multiple actions. Warning: In future versions of ECS, we plan to provide a list of acceptable values for this field, please use with caution.', - example: 'user-management', - name: 'category', - type: 'keyword', + category: 'host', + field: 'host.os.kernel', + values: '4.15.0-45-generic', + originalValue: '4.15.0-45-generic', }, { - category: 'event', - field: 'event.module', - values: 'auditd', - originalValue: 'auditd', - description: - 'Name of the module this data is coming from. This information is coming from the modules used in Beats or Logstash.', - example: 'mysql', - name: 'module', - type: 'keyword', + category: 'host', + field: 'host.os.codename', + values: 'bionic', + originalValue: 'bionic', }, { - category: 'event', - field: 'event.original', - values: [ - 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', - 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', - 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', - 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', - ], - originalValue: [ - 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', - 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', - 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', - 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', - ], - description: - 'Raw text message of entire event. Used to demonstrate log integrity. This field is not indexed and doc_values are disabled. It cannot be searched, but it can be retrieved from `_source`.', - example: - 'Sep 19 08:26:10 host CEF:0|Security| threatmanager|1.0|100| worm successfully stopped|10|src=10.0.0.1 dst=2.1.2.2spt=1232', - name: 'original', - type: 'keyword', + category: 'host', + field: 'host.os.platform', + values: 'ubuntu', + originalValue: 'ubuntu', }, { - category: 'file', - field: 'file.device', - values: '00:00', - originalValue: '00:00', - description: 'Device that is the source of the file.', - name: 'device', - type: 'keyword', + category: 'host', + field: 'host.os.version', + values: '18.04.2 LTS (Bionic Beaver)', + originalValue: '18.04.2 LTS (Bionic Beaver)', }, { - category: 'file', - field: 'file.gid', - values: '0', - originalValue: '0', - description: 'Primary group ID (GID) of the file.', - name: 'gid', - type: 'keyword', + category: 'host', + field: 'host.os.family', + values: 'debian', + originalValue: 'debian', + }, + { + category: 'host', + field: 'host.id', + values: '7c21f5ed03b04d0299569d221fe18bbc', + originalValue: '7c21f5ed03b04d0299569d221fe18bbc', + }, + { + category: 'host', + field: 'host.name', + values: 'zeek-london', + originalValue: 'zeek-london', + }, + { + category: 'host', + field: 'host.ip', + values: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + originalValue: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], + }, + { + category: 'host', + field: 'host.mac', + values: ['42:66:42:19:b3:b9'], + originalValue: ['42:66:42:19:b3:b9'], + }, + { + category: 'host', + field: 'host.hostname', + values: 'zeek-london', + originalValue: 'zeek-london', + }, + { + category: 'cloud', + field: 'cloud.provider', + values: 'digitalocean', + originalValue: 'digitalocean', + }, + { + category: 'cloud', + field: 'cloud.instance.id', + values: '136398786', + originalValue: '136398786', + }, + { + category: 'cloud', + field: 'cloud.region', + values: 'lon1', + originalValue: 'lon1', }, { category: 'file', - field: 'file.group', - values: 'root', - originalValue: 'root', - description: 'Primary group name of the file.', - name: 'group', - type: 'keyword', + field: 'file.device', + values: '00:00', + originalValue: '00:00', }, { category: 'file', field: 'file.inode', values: '3926', originalValue: '3926', - description: 'Inode representing the file in the filesystem.', - name: 'inode', - type: 'keyword', }, { category: 'file', field: 'file.mode', values: '0644', originalValue: '0644', - description: 'Mode of the file in octal representation.', - example: 416, - name: 'mode', - type: 'keyword', - }, - { - category: 'file', - field: 'file.owner', - values: 'root', - originalValue: 'root', - description: "File owner's username.", - name: 'owner', - type: 'keyword', - }, - { - category: 'file', - field: 'file.path', - values: '/etc/passwd', - originalValue: '/etc/passwd', - description: 'Path to the file.', - name: 'path', - type: 'keyword', }, { category: 'file', field: 'file.uid', values: '0', originalValue: '0', - description: 'The user ID (UID) or security identifier (SID) of the file owner.', - name: 'uid', - type: 'keyword', }, { - category: 'host', - field: 'host.architecture', - values: 'x86_64', - originalValue: 'x86_64', - description: 'Operating system architecture.', - example: 'x86_64', - name: 'architecture', - type: 'keyword', + category: 'file', + field: 'file.gid', + values: '0', + originalValue: '0', }, { - category: 'host', - field: 'host.hostname', - values: 'zeek-london', - originalValue: 'zeek-london', - description: - 'Hostname of the host. It normally contains what the `hostname` command returns on the host machine.', - name: 'hostname', - type: 'keyword', + category: 'file', + field: 'file.owner', + values: 'root', + originalValue: 'root', }, { - category: 'host', - field: 'host.id', - values: '7c21f5ed03b04d0299569d221fe18bbc', - originalValue: '7c21f5ed03b04d0299569d221fe18bbc', - description: - 'Unique host id. As hostname is not always unique, use values that are meaningful in your environment. Example: The current usage of `beat.name`.', - name: 'id', - type: 'keyword', + category: 'file', + field: 'file.group', + values: 'root', + originalValue: 'root', }, { - category: 'host', - field: 'host.ip', - values: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - originalValue: ['46.101.3.136', '10.16.0.5', 'fe80::4066:42ff:fe19:b3b9'], - description: 'Host ip address.', - name: 'ip', - type: 'ip', + category: 'file', + field: 'file.path', + values: '/etc/passwd', + originalValue: '/etc/passwd', }, { - category: 'host', - field: 'host.mac', - values: ['42:66:42:19:b3:b9'], - originalValue: ['42:66:42:19:b3:b9'], - description: 'Host mac address.', - name: 'mac', - type: 'keyword', + category: 'auditd', + field: 'auditd.session', + values: 'unset', + originalValue: 'unset', }, { - category: 'host', - field: 'host.name', - values: 'zeek-london', - originalValue: 'zeek-london', - description: - 'Name of the host. It can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', - name: 'name', - type: 'keyword', + category: 'auditd', + field: 'auditd.data.tty', + values: '(none)', + originalValue: '(none)', }, { - category: 'host', - field: 'host.os.codename', - values: 'bionic', - originalValue: 'bionic', - type: 'keyword', + category: 'auditd', + field: 'auditd.data.a3', + values: '0', + originalValue: '0', }, { - category: 'host', - field: 'host.os.family', - values: 'debian', - originalValue: 'debian', - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - name: 'family', - type: 'keyword', + category: 'auditd', + field: 'auditd.data.a2', + values: '80000', + originalValue: '80000', }, { - category: 'host', - field: 'host.os.kernel', - values: '4.15.0-45-generic', - originalValue: '4.15.0-45-generic', - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', - name: 'kernel', - type: 'keyword', + category: 'auditd', + field: 'auditd.data.syscall', + values: 'openat', + originalValue: 'openat', }, { - category: 'host', - field: 'host.os.name', - values: 'Ubuntu', - originalValue: 'Ubuntu', - description: 'Operating system name, without the version.', - example: 'Mac OS X', - name: 'name', - type: 'keyword', + category: 'auditd', + field: 'auditd.data.a1', + values: '7fe0f63df220', + originalValue: '7fe0f63df220', }, { - category: 'host', - field: 'host.os.platform', - values: 'ubuntu', - originalValue: 'ubuntu', - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', - name: 'platform', - type: 'keyword', + category: 'auditd', + field: 'auditd.data.a0', + values: 'ffffff9c', + originalValue: 'ffffff9c', }, { - category: 'host', - field: 'host.os.version', - values: '18.04.2 LTS (Bionic Beaver)', - originalValue: '18.04.2 LTS (Bionic Beaver)', - description: 'Operating system version as a raw string.', - example: '10.14.1', - name: 'version', - type: 'keyword', + category: 'auditd', + field: 'auditd.data.arch', + values: 'x86_64', + originalValue: 'x86_64', }, { - category: 'process', - field: 'process.executable', - values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', - originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', - type: 'keyword', + category: 'auditd', + field: 'auditd.data.exit', + values: '12', + originalValue: '12', }, { - category: 'process', - field: 'process.name', - values: 'auditbeat', - originalValue: 'auditbeat', - type: 'keyword', + category: 'auditd', + field: 'auditd.summary.actor.primary', + values: 'unset', + originalValue: 'unset', }, { - category: 'process', - field: 'process.pid', - values: 15990, - originalValue: 15990, - type: 'long', + category: 'auditd', + field: 'auditd.summary.actor.secondary', + values: 'root', + originalValue: 'root', }, { - category: 'process', - field: 'process.ppid', - values: 1, - originalValue: 1, - type: 'long', + category: 'auditd', + field: 'auditd.summary.object.primary', + values: '/etc/passwd', + originalValue: '/etc/passwd', }, { - category: 'process', - field: 'process.title', - values: - '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', - originalValue: - '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat -e -c /root/go/src/github.com/elastic/beats/x-pack/auditbeat/au', - type: 'keyword', + category: 'auditd', + field: 'auditd.summary.object.type', + values: 'file', + originalValue: 'file', }, { - category: 'process', - field: 'process.working_directory', - values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', - originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat', - type: 'keyword', + category: 'auditd', + field: 'auditd.summary.how', + values: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', + originalValue: '/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat', }, { - category: 'service', - field: 'service.type', - values: 'auditd', - originalValue: 'auditd', - description: - 'The type of the service data is collected from. The type can be used to group and correlate logs and metrics from one service type. Example: If logs or metrics are collected from Elasticsearch, `service.type` would be `elasticsearch`.', - example: 'elasticsearch', - name: 'type', - type: 'keyword', + category: 'auditd', + field: 'auditd.paths', + values: [ + { + rdev: '00:00', + cap_fe: '0', + nametype: 'NORMAL', + ogid: '0', + ouid: '0', + inode: '3926', + item: '0', + mode: '0100644', + name: '/etc/passwd', + cap_fi: '0000000000000000', + cap_fp: '0000000000000000', + cap_fver: '0', + dev: 'fc:01', + }, + ], + originalValue: [ + { + rdev: '00:00', + cap_fe: '0', + nametype: 'NORMAL', + ogid: '0', + ouid: '0', + inode: '3926', + item: '0', + mode: '0100644', + name: '/etc/passwd', + cap_fi: '0000000000000000', + cap_fp: '0000000000000000', + cap_fver: '0', + dev: 'fc:01', + }, + ], }, { - category: 'user', - field: 'user.audit.id', - values: 'unset', - originalValue: 'unset', - type: 'keyword', + category: 'auditd', + field: 'auditd.message_type', + values: 'syscall', + originalValue: 'syscall', }, { - category: 'user', - field: 'user.effective.group.id', - values: '0', - originalValue: '0', - type: 'keyword', + category: 'auditd', + field: 'auditd.sequence', + values: 8817905, + originalValue: 8817905, }, { - category: 'user', - field: 'user.effective.group.name', - values: 'root', - originalValue: 'root', - type: 'keyword', + category: 'auditd', + field: 'auditd.result', + values: 'success', + originalValue: 'success', }, { - category: 'user', - field: 'user.effective.id', - values: '0', - originalValue: '0', - type: 'keyword', + category: 'event', + field: 'event.category', + values: 'audit-rule', + originalValue: 'audit-rule', }, { - category: 'user', - field: 'user.effective.name', - values: 'root', - originalValue: 'root', - type: 'keyword', + category: 'event', + field: 'event.action', + values: 'opened-file', + originalValue: 'opened-file', }, { - category: 'user', - field: 'user.filesystem.group.id', - values: '0', - originalValue: '0', - type: 'keyword', + category: 'event', + field: 'event.original', + values: [ + 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', + 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', + 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', + 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', + ], + originalValue: [ + 'type=SYSCALL msg=audit(1553886083.420:8817905): arch=c000003e syscall=257 success=yes exit=12 a0=ffffff9c a1=7fe0f63df220 a2=80000 a3=0 items=1 ppid=1 pid=15990 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditbeat" exe="/root/go/src/github.com/elastic/beats/x-pack/auditbeat/auditbeat" key=(null)', + 'type=CWD msg=audit(1553886083.420:8817905): cwd="/root/go/src/github.com/elastic/beats/x-pack/auditbeat"', + 'type=PATH msg=audit(1553886083.420:8817905): item=0 name="/etc/passwd" inode=3926 dev=fc:01 mode=0100644 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0000000000000000 cap_fi=0000000000000000 cap_fe=0 cap_fver=0', + 'type=PROCTITLE msg=audit(1553886083.420:8817905): proctitle=2F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F617564697462656174002D65002D63002F726F6F742F676F2F7372632F6769746875622E636F6D2F656C61737469632F62656174732F782D7061636B2F6175646974626561742F6175', + ], }, { - category: 'user', - field: 'user.filesystem.group.name', - values: 'root', - originalValue: 'root', - type: 'keyword', + category: 'event', + field: 'event.module', + values: 'auditd', + originalValue: 'auditd', }, { - category: 'user', - field: 'user.filesystem.id', - values: '0', - originalValue: '0', - type: 'keyword', + category: 'ecs', + field: 'ecs.version', + values: '1.0.0', + originalValue: '1.0.0', }, { - category: 'user', - field: 'user.filesystem.name', - values: 'root', - originalValue: 'root', - type: 'keyword', + category: 'agent', + field: 'agent.ephemeral_id', + values: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', + originalValue: '6d541d59-52d0-4e70-b4d2-2660c0a99ff7', }, { - category: 'user', - field: 'user.group.id', - values: '0', - originalValue: '0', - description: 'Unique identifier for the group on the system/platform.', - name: 'id', - type: 'keyword', + category: 'agent', + field: 'agent.hostname', + values: 'zeek-london', + originalValue: 'zeek-london', }, { - category: 'user', - field: 'user.group.name', - values: 'root', - originalValue: 'root', - description: 'Name of the group.', - name: 'name', - type: 'keyword', + category: 'agent', + field: 'agent.id', + values: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', + originalValue: 'cc1f4183-36c6-45c4-b21b-7ce70c3572db', }, { - category: 'user', - field: 'user.id', - values: '0', - originalValue: '0', - description: 'One or multiple unique identifiers of the user.', - name: 'id', - type: 'keyword', + category: 'agent', + field: 'agent.version', + values: '8.0.0', + originalValue: '8.0.0', }, { - category: 'user', - field: 'user.name', - values: 'root', - originalValue: 'root', - description: 'Short name or login of the user.', - example: 'albert', - name: 'name', - type: 'keyword', + category: 'agent', + field: 'agent.type', + values: 'auditbeat', + originalValue: 'auditbeat', }, { - category: 'user', - field: 'user.saved.group.id', - values: '0', - originalValue: '0', - type: 'keyword', + category: '_index', + field: '_index', + values: 'auditbeat-8.0.0-2019.03.29-000003', + originalValue: 'auditbeat-8.0.0-2019.03.29-000003', }, { - category: 'user', - field: 'user.saved.group.name', - values: 'root', - originalValue: 'root', - type: 'keyword', + category: '_type', + field: '_type', + values: '_doc', + originalValue: '_doc', }, { - category: 'user', - field: 'user.saved.id', - values: '0', - originalValue: '0', - type: 'keyword', + category: '_id', + field: '_id', + values: 'TUfUymkBCQofM5eXGBYL', + originalValue: 'TUfUymkBCQofM5eXGBYL', }, { - category: 'user', - field: 'user.saved.name', - values: 'root', - originalValue: 'root', - type: 'keyword', + category: '_score', + field: '_score', + values: 1, + originalValue: 1, }, ], }; diff --git a/x-pack/legacy/plugins/siem/server/lib/note/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/note/saved_object.ts index 474ae1b829914..7c3b5a3f02612 100644 --- a/x-pack/legacy/plugins/siem/server/lib/note/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/note/saved_object.ts @@ -8,11 +8,18 @@ import { failure } from 'io-ts/lib/PathReporter'; import { RequestAuth } from 'hapi'; import { Legacy } from 'kibana'; import { getOr } from 'lodash/fp'; +import uuid from 'uuid'; import { SavedObjectsFindOptions } from 'src/core/server'; import { Pick3 } from '../../../common/utility_types'; -import { PageInfoNote, ResponseNote, ResponseNotes, SortNote } from '../../graphql/types'; +import { + PageInfoNote, + ResponseNote, + ResponseNotes, + SortNote, + NoteResult, +} from '../../graphql/types'; import { FrameworkRequest, internalFrameworkRequest } from '../framework'; import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types'; import { noteSavedObjectType } from './saved_object_mappings'; @@ -156,6 +163,20 @@ export class Note { ), }; } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { + const noteToReturn: NoteResult = { + ...note, + noteId: uuid.v1(), + version: '', + timelineId: '', + timelineVersion: '', + }; + return { + code: 403, + message: err.message, + note: noteToReturn, + }; + } throw err; } } diff --git a/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts index f62f36d32f52e..d07a51d7f94c0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/pinned_event/saved_object.ts @@ -18,7 +18,7 @@ import { PinnedEventSavedObjectRuntimeType, SavedPinnedEvent, } from './types'; -import { PageInfoNote, SortNote } from '../../graphql/types'; +import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types'; import { pinnedEventSavedObjectType, timelineSavedObjectType } from '../../saved_objects'; import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; @@ -96,7 +96,7 @@ export class PinnedEvent { pinnedEventId: string | null, eventId: string, timelineId: string | null - ): Promise { + ): Promise { try { if (pinnedEventId == null) { const timelineVersionSavedObject = @@ -148,6 +148,17 @@ export class PinnedEvent { await this.deletePinnedEventOnTimeline(request, [pinnedEventId]); return null; } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { + return pinnedEventId != null + ? { + code: 403, + message: err.message, + pinnedEventId: eventId, + timelineId: '', + timelineVersion: '', + } + : null; + } throw err; } } diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts index 433fc175b642f..4d3203e2c5570 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/saved_object.ts @@ -15,6 +15,7 @@ import { PageInfoTimeline, SortTimeline, ResponseFavoriteTimeline, + TimelineResult, } from '../../graphql/types'; import { FrameworkRequest, internalFrameworkRequest } from '../framework'; import { NoteSavedObject } from '../note/types'; @@ -77,54 +78,68 @@ export class Timeline { request: FrameworkRequest, timelineId: string | null ): Promise { - let timeline: SavedTimeline = {}; - if (timelineId != null) { - const { - eventIdToNoteIds, - notes, - noteIds, - pinnedEventIds, - pinnedEventsSaveObject, - savedObjectId, - version, - ...savedTimeline - } = await this.getBasicSavedTimeline(request, timelineId); - timelineId = savedObjectId; - timeline = savedTimeline; - } const userName = getOr(null, 'credentials.username', request[internalFrameworkRequest].auth); const fullName = getOr(null, 'credentials.fullname', request[internalFrameworkRequest].auth); - const userFavoriteTimeline = { - keySearch: userName != null ? convertStringToBase64(userName) : null, - favoriteDate: new Date().valueOf(), - fullName, - userName, - }; - if (timeline.favorite != null) { - const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( - user => user.userName === userName - ); + try { + let timeline: SavedTimeline = {}; + if (timelineId != null) { + const { + eventIdToNoteIds, + notes, + noteIds, + pinnedEventIds, + pinnedEventsSaveObject, + savedObjectId, + version, + ...savedTimeline + } = await this.getBasicSavedTimeline(request, timelineId); + timelineId = savedObjectId; + timeline = savedTimeline; + } - timeline.favorite = - alreadyExistsTimelineFavoriteByUser > -1 - ? [ - ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), - ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), - ] - : [...timeline.favorite, userFavoriteTimeline]; - } else if (timeline.favorite == null) { - timeline.favorite = [userFavoriteTimeline]; - } + const userFavoriteTimeline = { + keySearch: userName != null ? convertStringToBase64(userName) : null, + favoriteDate: new Date().valueOf(), + fullName, + userName, + }; + if (timeline.favorite != null) { + const alreadyExistsTimelineFavoriteByUser = timeline.favorite.findIndex( + user => user.userName === userName + ); - const persistResponse = await this.persistTimeline(request, timelineId, null, timeline); - return { - savedObjectId: persistResponse.timeline.savedObjectId, - version: persistResponse.timeline.version, - favorite: - persistResponse.timeline.favorite != null - ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) - : [], - }; + timeline.favorite = + alreadyExistsTimelineFavoriteByUser > -1 + ? [ + ...timeline.favorite.slice(0, alreadyExistsTimelineFavoriteByUser), + ...timeline.favorite.slice(alreadyExistsTimelineFavoriteByUser + 1), + ] + : [...timeline.favorite, userFavoriteTimeline]; + } else if (timeline.favorite == null) { + timeline.favorite = [userFavoriteTimeline]; + } + + const persistResponse = await this.persistTimeline(request, timelineId, null, timeline); + return { + savedObjectId: persistResponse.timeline.savedObjectId, + version: persistResponse.timeline.version, + favorite: + persistResponse.timeline.favorite != null + ? persistResponse.timeline.favorite.filter(fav => fav.userName === userName) + : [], + }; + } catch (err) { + if (getOr(null, 'output.statusCode', err) === 403) { + return { + savedObjectId: '', + version: '', + favorite: [], + code: 403, + message: err.message, + }; + } + throw err; + } } public async persistTimeline( @@ -133,27 +148,26 @@ export class Timeline { version: string | null, timeline: SavedTimeline ): Promise { - if (timelineId == null) { - // Create new timeline - return { - code: 200, - message: 'success', - timeline: convertSavedObjectToSavedTimeline( - await this.libs.savedObjects - .getScopedSavedObjectsClient(request[internalFrameworkRequest]) - .create( - timelineSavedObjectType, - pickSavedTimeline( - timelineId, - timeline, - request[internalFrameworkRequest].auth || null - ) - ) - ), - }; - } - try { + if (timelineId == null) { + // Create new timeline + return { + code: 200, + message: 'success', + timeline: convertSavedObjectToSavedTimeline( + await this.libs.savedObjects + .getScopedSavedObjectsClient(request[internalFrameworkRequest]) + .create( + timelineSavedObjectType, + pickSavedTimeline( + timelineId, + timeline, + request[internalFrameworkRequest].auth || null + ) + ) + ), + }; + } // Update Timeline await this.libs.savedObjects .getScopedSavedObjectsClient(request[internalFrameworkRequest]) @@ -171,12 +185,26 @@ export class Timeline { timeline: await this.getSavedTimeline(request, timelineId), }; } catch (err) { - if (this.libs.savedObjects.SavedObjectsClient.errors.isConflictError(err)) { + if ( + timelineId != null && + this.libs.savedObjects.SavedObjectsClient.errors.isConflictError(err) + ) { return { code: 409, message: err.message, timeline: await this.getSavedTimeline(request, timelineId), }; + } else if (getOr(null, 'output.statusCode', err) === 403) { + const timelineToReturn: TimelineResult = { + ...timeline, + savedObjectId: '', + version: '', + }; + return { + code: 403, + message: err.message, + timeline: timelineToReturn, + }; } throw err; } diff --git a/x-pack/test/api_integration/apis/siem/timeline_details.ts b/x-pack/test/api_integration/apis/siem/timeline_details.ts index 9f48c75ece0bc..d59c45c338c8c 100644 --- a/x-pack/test/api_integration/apis/siem/timeline_details.ts +++ b/x-pack/test/api_integration/apis/siem/timeline_details.ts @@ -15,10 +15,7 @@ import { import { KbnTestProvider } from './types'; type DetailsData = Array< - Pick< - DetailItem, - 'category' | 'description' | 'example' | 'field' | 'type' | 'values' | 'originalValue' - > & { + Pick & { __typename: string; } >; @@ -28,618 +25,277 @@ const INDEX_NAME = 'filebeat-7.0.0-iot-2019.06'; const ID = 'QRhG1WgBqd-n62SwZYDT'; const EXPECTED_DATA: DetailItem[] = [ { - category: '_id', - description: 'Each document has an _id that uniquely identifies it', - example: 'Y-6TfmcB0WOhS6qyMv3s', - field: '_id', - type: 'keyword', - originalValue: 'QRhG1WgBqd-n62SwZYDT', - values: ['QRhG1WgBqd-n62SwZYDT'], - }, - { - category: '_index', - description: - 'An index is like a ‘database’ in a relational database. It has a mapping which defines multiple types. An index is a logical namespace which maps to one or more primary shards and can have zero or more replica shards.', - example: 'auditbeat-8.0.0-2019.02.19-000001', - field: '_index', - type: 'keyword', - originalValue: 'filebeat-7.0.0-iot-2019.06', - values: ['filebeat-7.0.0-iot-2019.06'], - }, - { - category: '_type', - description: null, - example: null, - field: '_type', - type: 'keyword', - originalValue: '_doc', - values: ['_doc'], - }, - { - category: '_score', - description: null, - example: null, - field: '_score', - type: 'long', - originalValue: 1, - values: ['1'], - }, - { - category: '@timestamp', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', field: '@timestamp', - type: 'date', - originalValue: '2019-02-10T02:39:44.107Z', values: ['2019-02-10T02:39:44.107Z'], + originalValue: '2019-02-10T02:39:44.107Z', }, + { field: '@version', values: ['1'], originalValue: '1' }, { - category: '@version', - description: null, - example: null, - field: '@version', - type: 'keyword', - originalValue: '1', - values: ['1'], - }, - { - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', field: 'agent.ephemeral_id', - type: 'keyword', - originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', values: ['909cd6a1-527d-41a5-9585-a7fb5386f851'], + originalValue: '909cd6a1-527d-41a5-9585-a7fb5386f851', }, { - category: 'agent', - description: null, - example: null, field: 'agent.hostname', - type: 'keyword', - originalValue: 'raspberrypi', values: ['raspberrypi'], + originalValue: 'raspberrypi', }, { - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', field: 'agent.id', - type: 'keyword', - originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', values: ['4d3ea604-27e5-4ec7-ab64-44f82285d776'], + originalValue: '4d3ea604-27e5-4ec7-ab64-44f82285d776', }, { - category: 'agent', - description: - 'Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.', - example: 'filebeat', field: 'agent.type', - type: 'keyword', - originalValue: 'filebeat', values: ['filebeat'], + originalValue: 'filebeat', }, + { field: 'agent.version', values: ['7.0.0'], originalValue: '7.0.0' }, { - category: 'agent', - description: 'Version of the agent.', - example: '6.0.0-rc2', - field: 'agent.version', - type: 'keyword', - originalValue: '7.0.0', - values: ['7.0.0'], - }, - { - category: 'destination', - description: 'Destination domain.', - example: null, field: 'destination.domain', - type: 'keyword', - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', }, { - category: 'destination', - description: 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: null, field: 'destination.ip', - type: 'ip', - originalValue: '10.100.7.196', values: ['10.100.7.196'], + originalValue: '10.100.7.196', }, + { field: 'destination.port', values: ['40684'], originalValue: 40684 }, { - category: 'destination', - description: 'Port of the destination.', - example: null, - field: 'destination.port', - type: 'long', - originalValue: 40684, - values: ['40684'], - }, - { - category: 'ecs', - description: - 'ECS version this event conforms to. `ecs.version` is a required field and must exist in all events. When querying across multiple indices -- which may conform to slightly different ECS versions -- this field lets integrations adjust to the schema version of the events. The current version is 1.0.0-beta2 .', - example: '1.0.0-beta2', field: 'ecs.version', - type: 'keyword', - originalValue: '1.0.0-beta2', values: ['1.0.0-beta2'], + originalValue: '1.0.0-beta2', }, { - category: 'event', - description: - 'Name of the dataset. The concept of a `dataset` (fileset / metricset) is used in Beats as a subset of modules. It contains the information which is currently stored in metricset.name and metricset.module or fileset.name.', - example: 'stats', field: 'event.dataset', - type: 'keyword', - originalValue: 'suricata.eve', values: ['suricata.eve'], + originalValue: 'suricata.eve', }, { - category: 'event', - description: - 'event.end contains the date when the event ended or when the activity was last observed.', - example: null, field: 'event.end', - type: 'date', - originalValue: '2019-02-10T02:39:44.107Z', values: ['2019-02-10T02:39:44.107Z'], + originalValue: '2019-02-10T02:39:44.107Z', }, + { field: 'event.kind', values: ['event'], originalValue: 'event' }, { - category: 'event', - description: - 'The kind of the event. This gives information about what type of information the event contains, without being specific to the contents of the event. Examples are `event`, `state`, `alarm`. Warning: In future versions of ECS, we plan to provide a list of acceptable values for this field, please use with caution.', - example: 'state', - field: 'event.kind', - type: 'keyword', - originalValue: 'event', - values: ['event'], - }, - { - category: 'event', - description: - 'Name of the module this data is coming from. This information is coming from the modules used in Beats or Logstash.', - example: 'mysql', field: 'event.module', - type: 'keyword', - originalValue: 'suricata', values: ['suricata'], + originalValue: 'suricata', }, { - category: 'event', - description: 'Reserved for future usage. Please avoid using this field for user data.', - example: null, field: 'event.type', - type: 'keyword', - originalValue: 'fileinfo', values: ['fileinfo'], + originalValue: 'fileinfo', }, { - category: 'file', - description: 'Path to the file.', - example: null, field: 'file.path', - type: 'keyword', - originalValue: - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], + originalValue: + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', }, + { field: 'file.size', values: ['48277'], originalValue: 48277 }, + { field: 'fileset.name', values: ['eve'], originalValue: 'eve' }, + { field: 'flow.locality', values: ['public'], originalValue: 'public' }, { - category: 'file', - description: 'File size in bytes (field is only added when `type` is `file`).', - example: null, - field: 'file.size', - type: 'long', - originalValue: 48277, - values: ['48277'], - }, - { - category: 'fileset', - description: null, - example: null, - field: 'fileset.name', - type: 'keyword', - originalValue: 'eve', - values: ['eve'], - }, - { - category: 'flow', - description: null, - example: null, - field: 'flow.locality', - type: 'keyword', - originalValue: 'public', - values: ['public'], - }, - { - category: 'host', - description: 'Operating system architecture.', - example: 'x86_64', field: 'host.architecture', - type: 'keyword', - originalValue: 'armv7l', values: ['armv7l'], + originalValue: 'armv7l', }, { - category: 'host', - description: - 'Hostname of the host. It normally contains what the `hostname` command returns on the host machine.', - example: null, field: 'host.hostname', - type: 'keyword', - originalValue: 'raspberrypi', values: ['raspberrypi'], + originalValue: 'raspberrypi', }, { - category: 'host', - description: - 'Unique host id. As hostname is not always unique, use values that are meaningful in your environment. Example: The current usage of `beat.name`.', - example: null, field: 'host.id', - type: 'keyword', - originalValue: 'b19a781f683541a7a25ee345133aa399', values: ['b19a781f683541a7a25ee345133aa399'], + originalValue: 'b19a781f683541a7a25ee345133aa399', }, { - category: 'host', - description: - 'Name of the host. It can contain what `hostname` returns on Unix systems, the fully qualified domain name, or a name specified by the user. The sender decides which value to use.', - example: null, field: 'host.name', - type: 'keyword', - originalValue: 'raspberrypi', values: ['raspberrypi'], + originalValue: 'raspberrypi', }, { - category: 'host', - description: null, - example: null, field: 'host.os.codename', - type: 'keyword', - originalValue: 'stretch', values: ['stretch'], + originalValue: 'stretch', }, + { field: 'host.os.family', values: [''], originalValue: '' }, { - category: 'host', - description: 'OS family (such as redhat, debian, freebsd, windows).', - example: 'debian', - field: 'host.os.family', - type: 'keyword', - originalValue: '', - values: [''], - }, - { - category: 'host', - description: 'Operating system kernel version as a raw string.', - example: '4.4.0-112-generic', field: 'host.os.kernel', - type: 'keyword', - originalValue: '4.14.50-v7+', values: ['4.14.50-v7+'], + originalValue: '4.14.50-v7+', }, { - category: 'host', - description: 'Operating system name, without the version.', - example: 'Mac OS X', field: 'host.os.name', - type: 'keyword', - originalValue: 'Raspbian GNU/Linux', values: ['Raspbian GNU/Linux'], + originalValue: 'Raspbian GNU/Linux', }, { - category: 'host', - description: 'Operating system platform (such centos, ubuntu, windows).', - example: 'darwin', field: 'host.os.platform', - type: 'keyword', - originalValue: 'raspbian', values: ['raspbian'], + originalValue: 'raspbian', }, { - category: 'host', - description: 'Operating system version as a raw string.', - example: '10.14.1', field: 'host.os.version', - type: 'keyword', - originalValue: '9 (stretch)', values: ['9 (stretch)'], + originalValue: '9 (stretch)', }, + { field: 'http.request.method', values: ['get'], originalValue: 'get' }, { - category: 'http', - description: - 'Http request method. The field value must be normalized to lowercase for querying. See "Lowercase Capitalization" in the "Implementing ECS" section.', - example: 'get, post, put', - field: 'http.request.method', - type: 'keyword', - originalValue: 'get', - values: ['get'], - }, - { - category: 'http', - description: 'Size in bytes of the response body.', - example: '887', field: 'http.response.body.bytes', - type: 'long', - originalValue: 48277, values: ['48277'], + originalValue: 48277, }, { - category: 'http', - description: 'Http response status code.', - example: '404', field: 'http.response.status_code', - type: 'long', - originalValue: 206, values: ['206'], + originalValue: 206, }, + { field: 'input.type', values: ['log'], originalValue: 'log' }, { - category: 'input', - description: null, - example: null, - field: 'input.type', - type: 'keyword', - originalValue: 'log', - values: ['log'], - }, - { - category: 'labels', - description: null, - example: null, field: 'labels.pipeline', - type: 'keyword', - originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', values: ['filebeat-7.0.0-suricata-eve-pipeline'], + originalValue: 'filebeat-7.0.0-suricata-eve-pipeline', }, { - category: 'log', - description: null, - example: null, field: 'log.file.path', - type: 'keyword', - originalValue: '/var/log/suricata/eve.json', values: ['/var/log/suricata/eve.json'], + originalValue: '/var/log/suricata/eve.json', }, { - category: 'log', - description: null, - example: null, field: 'log.offset', - type: 'long', - originalValue: 1856288115, values: ['1856288115'], + originalValue: 1856288115, }, + { field: 'network.name', values: ['iot'], originalValue: 'iot' }, + { field: 'network.protocol', values: ['http'], originalValue: 'http' }, + { field: 'network.transport', values: ['tcp'], originalValue: 'tcp' }, { - category: 'network', - description: 'Name given by operators to sections of their network.', - example: 'Guest Wifi', - field: 'network.name', - type: 'keyword', - originalValue: 'iot', - values: ['iot'], - }, - { - category: 'network', - description: - 'L7 Network protocol name. ex. http, lumberjack, transport protocol. The field value must be normalized to lowercase for querying. See "Lowercase Capitalization" in the "Implementing ECS" section.', - example: 'http', - field: 'network.protocol', - type: 'keyword', - originalValue: 'http', - values: ['http'], - }, - { - category: 'network', - description: - 'Same as network.iana_number, but instead using the Keyword name of the transport layer (udp, tcp, ipv6-icmp, etc.) The field value must be normalized to lowercase for querying. See "Lowercase Capitalization" in the "Implementing ECS" section.', - example: 'tcp', - field: 'network.transport', - type: 'keyword', - originalValue: 'tcp', - values: ['tcp'], - }, - { - category: 'service', - description: - 'The type of the service data is collected from. The type can be used to group and correlate logs and metrics from one service type. Example: If logs or metrics are collected from Elasticsearch, `service.type` would be `elasticsearch`.', - example: 'elasticsearch', field: 'service.type', - type: 'keyword', - originalValue: 'suricata', values: ['suricata'], + originalValue: 'suricata', }, + { field: 'source.as.num', values: ['16509'], originalValue: 16509 }, { - category: 'source', - description: null, - example: null, - field: 'source.as.num', - type: 'long', - originalValue: 16509, - values: ['16509'], - }, - { - category: 'source', - description: null, - example: null, field: 'source.as.org', - type: 'keyword', - originalValue: 'Amazon.com, Inc.', values: ['Amazon.com, Inc.'], + originalValue: 'Amazon.com, Inc.', }, { - category: 'source', - description: 'Source domain.', - example: null, field: 'source.domain', - type: 'keyword', - originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', values: ['server-54-239-219-210.jfk51.r.cloudfront.net'], + originalValue: 'server-54-239-219-210.jfk51.r.cloudfront.net', }, { - category: 'source', - description: 'City name.', - example: 'Montreal', field: 'source.geo.city_name', - type: 'keyword', - originalValue: 'Seattle', values: ['Seattle'], + originalValue: 'Seattle', }, { - category: 'source', - description: 'Name of the continent.', - example: 'North America', field: 'source.geo.continent_name', - type: 'keyword', - originalValue: 'North America', values: ['North America'], + originalValue: 'North America', }, { - category: 'source', - description: 'Country ISO code.', - example: 'CA', field: 'source.geo.country_iso_code', - type: 'keyword', - originalValue: 'US', values: ['US'], + originalValue: 'US', + }, + { + field: 'source.geo.location.lat', + values: ['47.6103'], + originalValue: 47.6103, + }, + { + field: 'source.geo.location.lon', + values: ['-122.3341'], + originalValue: -122.3341, }, { - category: 'source', - description: 'Region ISO code.', - example: 'CA-QC', field: 'source.geo.region_iso_code', - type: 'keyword', - originalValue: 'US-WA', values: ['US-WA'], + originalValue: 'US-WA', }, { - category: 'source', - description: 'Region name.', - example: 'Quebec', field: 'source.geo.region_name', - type: 'keyword', - originalValue: 'Washington', values: ['Washington'], + originalValue: 'Washington', }, { - category: 'source', - description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: null, field: 'source.ip', - type: 'ip', - originalValue: '54.239.219.210', values: ['54.239.219.210'], + originalValue: '54.239.219.210', }, + { field: 'source.port', values: ['80'], originalValue: 80 }, { - category: 'source', - description: 'Port of the source.', - example: null, - field: 'source.port', - type: 'long', - originalValue: 80, - values: ['80'], - }, - { - category: 'suricata', - description: null, - example: null, field: 'suricata.eve.fileinfo.state', - type: 'keyword', - originalValue: 'CLOSED', values: ['CLOSED'], + originalValue: 'CLOSED', }, { - category: 'suricata', - description: null, - example: null, field: 'suricata.eve.fileinfo.tx_id', - type: 'long', - originalValue: 301, values: ['301'], + originalValue: 301, }, { - category: 'suricata', - description: null, - example: null, field: 'suricata.eve.flow_id', - type: 'keyword', - originalValue: 196625917175466, values: ['196625917175466'], + originalValue: 196625917175466, }, { - category: 'suricata', - description: null, - example: null, field: 'suricata.eve.http.http_content_type', - type: 'keyword', - originalValue: 'video/mp4', values: ['video/mp4'], + originalValue: 'video/mp4', }, { - category: 'suricata', - description: null, - example: null, field: 'suricata.eve.http.protocol', - type: 'keyword', - originalValue: 'HTTP/1.1', values: ['HTTP/1.1'], + originalValue: 'HTTP/1.1', }, { - category: 'suricata', - description: null, - example: null, field: 'suricata.eve.in_iface', - type: 'keyword', - originalValue: 'eth0', values: ['eth0'], + originalValue: 'eth0', }, + { field: 'tags', values: ['suricata'], originalValue: ['suricata'] }, { - category: 'tags', - description: 'List of keywords used to tag each event.', - example: '["production", "env2"]', - field: 'tags', - type: 'keyword', - originalValue: ['suricata'], - values: ['suricata'], - }, - { - category: 'url', - description: - 'Domain of the request, such as "www.elastic.co". In some cases a URL may refer to an IP and/or port directly, without a domain name. In this case, the IP address would go to the `domain` field.', - example: 'www.elastic.co', field: 'url.domain', - type: 'keyword', - originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', values: ['s3-iad-2.cf.dash.row.aiv-cdn.net'], + originalValue: 's3-iad-2.cf.dash.row.aiv-cdn.net', }, { - category: 'url', - description: - 'Unmodified original url as seen in the event source. Note that in network monitoring, the observed URL may be a full URL, whereas in access logs, the URL is often just represented as a path. This field is meant to represent the URL as it was observed, complete or not.', - example: 'https://www.elastic.co:443/search?q=elasticsearch#top or /search?q=elasticsearch', field: 'url.original', - type: 'keyword', - originalValue: - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], + originalValue: + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', }, { - category: 'url', - description: 'Path of the request, such as "/search".', - example: null, field: 'url.path', - type: 'keyword', - originalValue: - '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', values: [ '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', ], + originalValue: + '/dm/2$XTMWANo0Q2RZKlH-95UoAahZrOg~/0a9a/bf72/e1da/4c20-919e-0cbabcf7bfe8/75f50c57-d25f-4e97-9e37-01b9f5caa293_audio_13.mp4', + }, + { + field: '_index', + values: ['filebeat-7.0.0-iot-2019.06'], + originalValue: 'filebeat-7.0.0-iot-2019.06', + }, + { field: '_type', values: ['_doc'], originalValue: '_doc' }, + { + field: '_id', + values: ['QRhG1WgBqd-n62SwZYDT'], + originalValue: 'QRhG1WgBqd-n62SwZYDT', }, + { field: '_score', values: ['1'], originalValue: 1 }, ]; const timelineDetailsTests: KbnTestProvider = ({ getService }) => { From 2ca827726c6ea27c82d9c95529af030f427807f2 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 24 Jul 2019 06:48:59 -0600 Subject: [PATCH 3/7] [Maps] clean up tooltip header and footer (#41793) (#41816) --- .../feature_tooltip.test.js.snap | 90 ++++++++++--------- .../map/feature_tooltip.js | 32 +++---- .../map/feature_tooltip.test.js | 7 -- 3 files changed, 64 insertions(+), 65 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/__snapshots__/feature_tooltip.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/map/__snapshots__/feature_tooltip.test.js.snap index fa9511840f253..f274fb2c801eb 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/__snapshots__/feature_tooltip.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/__snapshots__/feature_tooltip.test.js.snap @@ -18,17 +18,20 @@ exports[`FeatureTooltip (multi) should not show close button / should show count justifyContent="spaceBetween" > - - - 1 - - of - - 3 - - + + - + @@ -75,17 +77,20 @@ exports[`FeatureTooltip (multi) should show close button / should show count / s justifyContent="spaceBetween" > - - - 1 - - of - - 3 - - + + - + @@ -139,17 +143,20 @@ exports[`FeatureTooltip (multi) should show close button / should show count 1`] justifyContent="spaceBetween" > - - - 1 - - of - - 3 - - + + - + diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js index 6c9710b661840..b174b69ea9d84 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js @@ -7,17 +7,17 @@ import React, { Fragment } from 'react'; import { EuiButtonIcon, - EuiText, EuiPagination, EuiSelect, EuiIconTip, EuiHorizontalRule, EuiFlexGroup, - EuiFlexItem + EuiFlexItem, + EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FeatureProperties } from './feature_properties'; - +import { FormattedMessage } from '@kbn/i18n/react'; const ALL_LAYERS = '_ALL_LAYERS_'; const DEFAULT_PAGE_NUMBER = 0; @@ -48,7 +48,6 @@ export class FeatureTooltip extends React.Component { } _onLayerChange = (e) => { - const layerId = e.target.value; if (this.state.layerIdFilter === layerId) { return; @@ -70,14 +69,12 @@ export class FeatureTooltip extends React.Component { }; _loadUniqueLayers = async () => { - if (this._prevFeatures === this.props.features) { return; } this._prevFeatures = this.props.features; - const countByLayerId = new Map(); for (let i = 0; i < this.props.features.length; i++) { let count = countByLayerId.get(this.props.features[i].layerId); @@ -115,7 +112,6 @@ export class FeatureTooltip extends React.Component { } }; - _renderProperties(features) { const feature = features[this.state.pageNumber]; if (!feature) { @@ -159,6 +155,7 @@ export class FeatureTooltip extends React.Component { onChange={this._onLayerChange} valueOfSelected={this.state.layerIdFilter} compressed + fullWidth aria-label={i18n.translate('xpack.maps.tooltip.layerFilterLabel', { defaultMessage: 'Filter results by layer' })} @@ -167,7 +164,6 @@ export class FeatureTooltip extends React.Component { } _renderHeader() { - if (!this.props.isLocked) { return null; } @@ -176,8 +172,8 @@ export class FeatureTooltip extends React.Component { : null; return ( - - + + {this._renderLayerFilterBox()} @@ -190,7 +186,6 @@ export class FeatureTooltip extends React.Component { } _renderFooter(filteredFeatures) { - if (filteredFeatures.length === 1) { return null; } @@ -216,7 +211,6 @@ export class FeatureTooltip extends React.Component { ); } - _onPageChange = (pageNumber) => { this.setState({ pageNumber: pageNumber, @@ -234,9 +228,17 @@ export class FeatureTooltip extends React.Component { } _renderPagination(filteredFeatures) { - const pageNumberReadout = ( - {(this.state.pageNumber + 1)} of {filteredFeatures.length} + + + ); const cycleArrows = (this.props.isLocked) ? ( ); - } render() { @@ -284,4 +285,3 @@ export class FeatureTooltip extends React.Component { ); } } - diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.test.js index 9203c4f0dd5ad..62bfda04b0ec3 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.test.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.test.js @@ -9,17 +9,14 @@ import { shallow } from 'enzyme'; import { FeatureTooltip } from './feature_tooltip'; class MockLayer { - constructor(id) { this._id = id; } async getDisplayName() { return `display + ${this._id}`; } - } - const MULTI_FEATURE_MULTI_LAYER = [ { 'id': 'feature1', @@ -88,7 +85,6 @@ describe('FeatureTooltip (single)', () => { expect(component) .toMatchSnapshot(); }); - }); describe('FeatureTooltip (multi)', () => { @@ -145,7 +141,4 @@ describe('FeatureTooltip (multi)', () => { expect(component) .toMatchSnapshot(); }); - - - }); From 3f71e5f2eec415fd03ba7b5b8d0614234cc7c199 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 24 Jul 2019 06:49:14 -0600 Subject: [PATCH 4/7] [Maps] refactor filter actions to use embeddable actions (#41713) (#41821) * [Maps] refactor filters to use filter actions * remove isFilterable from redux state * more removal of set filterable --- .../plugins/maps/public/actions/ui_actions.js | 8 ---- .../connected_components/gis_map/view.js | 5 ++- .../map/feature_properties.js | 7 ++-- .../map/feature_properties.test.js | 4 -- .../map/feature_tooltip.js | 1 + .../connected_components/map/mb/index.js | 2 - .../connected_components/map/mb/view.js | 6 +-- .../toolbar_overlay/index.js | 2 - .../toolbar_overlay/toolbar_overlay.js | 4 +- .../maps/public/embeddable/map_embeddable.js | 19 +++++++-- .../plugins/maps/public/kibana_services.js | 3 -- .../layers/tooltips/es_tooltip_property.js | 20 ++++------ .../layers/tooltips/join_tooltip_property.js | 40 ++++++++----------- .../layers/tooltips/tooltip_property.js | 4 +- .../legacy/plugins/maps/public/reducers/ui.js | 4 -- .../maps/public/selectors/ui_selectors.js | 1 - 16 files changed, 52 insertions(+), 78 deletions(-) diff --git a/x-pack/legacy/plugins/maps/public/actions/ui_actions.js b/x-pack/legacy/plugins/maps/public/actions/ui_actions.js index 659e177158a3b..93bd4e4a1331f 100644 --- a/x-pack/legacy/plugins/maps/public/actions/ui_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/ui_actions.js @@ -10,7 +10,6 @@ export const OPEN_SET_VIEW = 'OPEN_SET_VIEW'; export const SET_IS_LAYER_TOC_OPEN = 'SET_IS_LAYER_TOC_OPEN'; export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'; export const SET_READ_ONLY = 'SET_READ_ONLY'; -export const SET_FILTERABLE = 'IS_FILTERABLE'; export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; export const SHOW_TOC_DETAILS = 'SHOW_TOC_DETAILS'; export const HIDE_TOC_DETAILS = 'HIDE_TOC_DETAILS'; @@ -57,13 +56,6 @@ export function setReadOnly(isReadOnly) { }; } -export function setFilterable(isFilterable) { - return { - type: SET_FILTERABLE, - isFilterable - }; -} - export function setOpenTOCDetails(layerIds) { return { type: SET_OPEN_TOC_DETAILS, diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js index 6b1d572cbde77..66010e2b2407b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js @@ -83,6 +83,7 @@ export class GisMap extends Component { render() { const { + addFilters, layerDetailsVisible, addLayerVisible, noFlyoutVisible, @@ -139,8 +140,8 @@ export class GisMap extends Component { data-shared-item > - - + + diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.js b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.js index 079684692e23f..a48760a631c9b 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.js @@ -81,7 +81,6 @@ export class FeatureProperties extends React.Component { } }; - _renderFilterCell(tooltipProperty) { if (!this.props.showFilterButtons || !tooltipProperty.isFilterable()) { return null; @@ -95,10 +94,10 @@ export class FeatureProperties extends React.Component { title={i18n.translate('xpack.maps.tooltip.filterOnPropertyTitle', { defaultMessage: 'Filter on property' })} - onClick={() => { + onClick={async () => { this.props.onCloseTooltip(); - const filterAction = tooltipProperty.getFilterAction(); - filterAction(); + const filters = await tooltipProperty.getESFilters(); + this.props.addFilters(filters); }} aria-label={i18n.translate('xpack.maps.tooltip.filterOnPropertyAriaLabel', { defaultMessage: 'Filter on property' diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.test.js b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.test.js index ff7c429b1576f..522e37f895ba3 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.test.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_properties.test.js @@ -19,10 +19,6 @@ class MockTooltipProperty { return this._isFilterable; } - getFilterAction() { - return () => {}; - } - getHtmlDisplayValue() { return this._value; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js index b174b69ea9d84..fd03fb288ece7 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/feature_tooltip.js @@ -124,6 +124,7 @@ export class FeatureTooltip extends React.Component { loadFeatureProperties={this.props.loadFeatureProperties} showFilterButtons={this.props.showFilterButtons} onCloseTooltip={this._onCloseTooltip} + addFilters={this.props.addFilters} /> ); } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js index 73dcd2f963ad4..5188c3a4ebbdb 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/index.js @@ -25,12 +25,10 @@ import { getDrawState, getScrollZoom } from '../../../selectors/map_selectors'; -import { getIsFilterable } from '../../../selectors/ui_selectors'; import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; function mapStateToProps(state = {}) { return { - isFilterable: getIsFilterable(state), isMapReady: getMapReady(state), layerList: getLayerList(state), goto: getGoto(state), diff --git a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js index 67f61e2e80703..befc0d82c26b3 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/map/mb/view.js @@ -19,7 +19,6 @@ import MapboxDraw from '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw-unminified'; import DrawRectangle from 'mapbox-gl-draw-rectangle-mode'; import { FeatureTooltip } from '../feature_tooltip'; import { DRAW_TYPE } from '../../../actions/map_actions'; -import { filterBarQueryFilter } from '../../../kibana_services'; import { createShapeFilterWithMeta, createExtentFilterWithMeta } from '../../../elasticsearch_geo_utils'; const mbDrawModes = MapboxDraw.modes; @@ -109,7 +108,7 @@ export class MBMapContainer extends React.Component { return; } - filterBarQueryFilter.addFilters([filter]); + this.props.addFilters([filter]); }; _debouncedSync = _.debounce(() => { @@ -415,8 +414,9 @@ export class MBMapContainer extends React.Component { loadFeatureProperties={this._loadFeatureProperties} findLayerById={this._findLayerById} closeTooltip={this._onTooltipClose} - showFilterButtons={this.props.isFilterable && isLocked} + showFilterButtons={!!this.props.addFilters && isLocked} isLocked={isLocked} + addFilters={this.props.addFilters} /> ), this._tooltipContainer); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/index.js b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/index.js index e0f4dd2feedb1..e27d0d3df2b63 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/index.js @@ -7,11 +7,9 @@ import { connect } from 'react-redux'; import { ToolbarOverlay } from './toolbar_overlay'; import { getUniqueIndexPatternIds } from '../../selectors/map_selectors'; -import { getIsFilterable } from '../../selectors/ui_selectors'; function mapStateToProps(state = {}) { return { - isFilterable: getIsFilterable(state), uniqueIndexPatternIds: getUniqueIndexPatternIds(state) }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js index 970c06c600a2e..b83a2deda15eb 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.js @@ -31,7 +31,7 @@ export class ToolbarOverlay extends React.Component { } componentDidUpdate() { - if (this.props.isFilterable) { + if (!!this.props.addFilters) { const nextUniqueIndexPatternIds = _.get(this.props, 'uniqueIndexPatternIds', []); this._loadUniqueIndexPatternAndFieldCombos(nextUniqueIndexPatternIds); } @@ -77,7 +77,7 @@ export class ToolbarOverlay extends React.Component { _renderToolsControl() { const { uniqueIndexPatternsAndGeoFields } = this.state; if ( - !this.props.isFilterable || + !this.props.addFilters || !uniqueIndexPatternsAndGeoFields.length ) { return null; diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js index a9e574fdbf505..da555d35d70ca 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js @@ -10,7 +10,11 @@ import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; import 'mapbox-gl/dist/mapbox-gl.css'; -import { Embeddable } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; +import { + APPLY_FILTER_TRIGGER, + Embeddable, + executeTriggerActions +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; import { I18nContext } from 'ui/i18n'; import { GisMap } from '../connected_components/gis_map'; @@ -26,7 +30,6 @@ import { import { DEFAULT_IS_LAYER_TOC_OPEN } from '../reducers/ui'; import { setReadOnly, - setFilterable, setIsLayerTOCOpen, setOpenTOCDetails, } from '../actions/ui_actions'; @@ -97,7 +100,6 @@ export class MapEmbeddable extends Embeddable { */ render(domNode) { this._store.dispatch(setReadOnly(true)); - this._store.dispatch(setFilterable(true)); this._store.dispatch(disableScrollZoom()); if (_.has(this.input, 'isLayerTOCOpen')) { @@ -136,7 +138,7 @@ export class MapEmbeddable extends Embeddable { render( - + , domNode @@ -147,6 +149,15 @@ export class MapEmbeddable extends Embeddable { }); } + addFilters = filters => { + executeTriggerActions(APPLY_FILTER_TRIGGER, { + embeddable: this, + triggerContext: { + filters, + }, + }); + } + destroy() { super.destroy(); if (this._unsubscribeFromStore) { diff --git a/x-pack/legacy/plugins/maps/public/kibana_services.js b/x-pack/legacy/plugins/maps/public/kibana_services.js index 0ed97610ac8fe..515e20cff9369 100644 --- a/x-pack/legacy/plugins/maps/public/kibana_services.js +++ b/x-pack/legacy/plugins/maps/public/kibana_services.js @@ -6,7 +6,6 @@ import { uiModules } from 'ui/modules'; import { SearchSourceProvider } from 'ui/courier'; -import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter'; import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; export { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { data } from 'plugins/data/setup'; @@ -14,7 +13,6 @@ import { data } from 'plugins/data/setup'; export const indexPatternService = data.indexPatterns.indexPatterns; export let SearchSource; -export let filterBarQueryFilter; export async function fetchSearchSourceAndRecordWithInspector({ searchSource, requestId, requestName, requestDesc, inspectorAdapters }) { const inspectorRequest = inspectorAdapters.requests.start( @@ -41,5 +39,4 @@ export async function fetchSearchSourceAndRecordWithInspector({ searchSource, re uiModules.get('app/maps').run(($injector) => { const Private = $injector.get('Private'); SearchSource = Private(SearchSourceProvider); - filterBarQueryFilter = Private(FilterBarQueryFilterProvider); }); diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_tooltip_property.js b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_tooltip_property.js index 9f55439a49374..ecb3a4b118c96 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/es_tooltip_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/es_tooltip_property.js @@ -5,7 +5,6 @@ */ import { buildPhraseFilter } from '@kbn/es-query'; -import { filterBarQueryFilter } from '../../kibana_services'; import { TooltipProperty } from './tooltip_property'; import _ from 'lodash'; @@ -35,17 +34,12 @@ export class ESTooltipProperty extends TooltipProperty { return field && (field.type === 'string' || field.type === 'date' || field.type === 'ip' || field.type === 'number'); } - getESFilter() { - return buildPhraseFilter( - this._indexPattern.fields.byName[this._propertyName], - this._rawValue, - this._indexPattern); - } - - getFilterAction() { - return () => { - const phraseFilter = this.getESFilter(); - filterBarQueryFilter.addFilters([phraseFilter]); - }; + async getESFilters() { + return [ + buildPhraseFilter( + this._indexPattern.fields.byName[this._propertyName], + this._rawValue, + this._indexPattern) + ]; } } diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js b/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js index d7639d26f4e17..cc19521063f36 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/join_tooltip_property.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ - -import { filterBarQueryFilter } from '../../kibana_services'; import { TooltipProperty } from './tooltip_property'; export class JoinTooltipProperty extends TooltipProperty { @@ -32,29 +30,23 @@ export class JoinTooltipProperty extends TooltipProperty { return this._tooltipProperty.getHtmlDisplayValue(); } - getFilterAction() { - //dispatch all the filter actions to the query bar - //this relies on the de-duping of filterBarQueryFilter - return async () => { - const esFilters = []; - if (this._tooltipProperty.isFilterable()) { - esFilters.push(this._tooltipProperty.getESFilter()); + async getESFilters() { + const esFilters = []; + if (this._tooltipProperty.isFilterable()) { + esFilters.push(...(await this._tooltipProperty.getESFilters())); + } + + for (let i = 0; i < this._leftInnerJoins.length; i++) { + const rightSource = this._leftInnerJoins[i].getRightJoinSource(); + const esTooltipProperty = await rightSource.createESTooltipProperty( + rightSource.getTerm(), + this._tooltipProperty.getRawValue() + ); + if (esTooltipProperty) { + esFilters.push(...(await esTooltipProperty.getESFilters())); } + } - for (let i = 0; i < this._leftInnerJoins.length; i++) { - const rightSource = this._leftInnerJoins[i].getRightJoinSource(); - const esTooltipProperty = await rightSource.createESTooltipProperty( - rightSource.getTerm(), - this._tooltipProperty.getRawValue() - ); - if (esTooltipProperty) { - const filter = esTooltipProperty.getESFilter(); - esFilters.push(filter); - } - } - filterBarQueryFilter.addFilters(esFilters); - }; + return esFilters; } - - } diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.js b/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.js index a9448c1413f61..401b623af226f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.js @@ -34,7 +34,7 @@ export class TooltipProperty { return false; } - getFilterAction() { - throw new Error('This property is not filterable'); + async getESFilters() { + return []; } } diff --git a/x-pack/legacy/plugins/maps/public/reducers/ui.js b/x-pack/legacy/plugins/maps/public/reducers/ui.js index 6a69bf94d00b8..696d938a68cd8 100644 --- a/x-pack/legacy/plugins/maps/public/reducers/ui.js +++ b/x-pack/legacy/plugins/maps/public/reducers/ui.js @@ -11,7 +11,6 @@ import { SET_IS_LAYER_TOC_OPEN, SET_FULL_SCREEN, SET_READ_ONLY, - SET_FILTERABLE, SET_OPEN_TOC_DETAILS, SHOW_TOC_DETAILS, HIDE_TOC_DETAILS, @@ -38,7 +37,6 @@ const INITIAL_STATE = { isFullScreen: false, isReadOnly: false, isLayerTOCOpen: DEFAULT_IS_LAYER_TOC_OPEN, - isFilterable: false, isSetViewOpen: false, // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // This also makes for easy read/write access for embeddables. @@ -61,8 +59,6 @@ export function ui(state = INITIAL_STATE, action) { return { ...state, isFullScreen: action.isFullScreen }; case SET_READ_ONLY: return { ...state, isReadOnly: action.isReadOnly }; - case SET_FILTERABLE: - return { ...state, isFilterable: action.isFilterable }; case SET_OPEN_TOC_DETAILS: return { ...state, openTOCDetails: action.layerIds }; case SHOW_TOC_DETAILS: diff --git a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js index 4707d49d9ebf5..912ee08396212 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js +++ b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js @@ -10,5 +10,4 @@ export const getIsLayerTOCOpen = ({ ui }) => ui.isLayerTOCOpen; export const getOpenTOCDetails = ({ ui }) => ui.openTOCDetails; export const getIsFullScreen = ({ ui }) => ui.isFullScreen; export const getIsReadOnly = ({ ui }) => ui.isReadOnly; -export const getIsFilterable = ({ ui }) => ui.isFilterable; export const getIndexingStage = ({ ui }) => ui.importIndexingStage; From 41739096a4c80d34214ac976c0ef9436a208a348 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Wed, 24 Jul 2019 10:13:40 -0400 Subject: [PATCH 5/7] [Uptime] Improve `useUrlParams` hook for Uptime app (#41545) (#41818) * WIP moving things around. * Refactor useUrlParams hook and usages in Uptime app. * Add unit test for new url params hook. * Update test snapshots. * Resolving some issues. * Add ReactRouter objects to RefreshContext. * Update url helper and context to better handle cases, simplify hook. * Add tests for helper function. * Update url params hook unit tests. * Replace outdated snapshot. --- .../functional/uptime_date_picker.tsx | 10 +- .../plugins/uptime/public/contexts/index.ts | 2 +- .../public/contexts/uptime_refresh_context.ts | 10 ++ .../contexts/uptime_settings_context.ts | 4 +- .../__snapshots__/use_url_params.test.ts.snap | 12 --- .../use_url_params.test.tsx.snap | 23 ++++ .../hooks/__tests__/use_url_params.test.ts | 30 ------ .../hooks/__tests__/use_url_params.test.tsx | 101 ++++++++++++++++++ .../uptime/public/hooks/use_url_params.ts | 60 ++++++----- .../get_supported_url_params.test.ts.snap | 33 ++++++ .../get_supported_url_params.test.ts | 21 ++++ .../url_params/get_supported_url_params.ts | 30 +++++- .../plugins/uptime/public/pages/monitor.tsx | 5 +- .../plugins/uptime/public/pages/overview.tsx | 11 +- .../plugins/uptime/public/uptime_app.tsx | 76 ++++++------- 15 files changed, 297 insertions(+), 131 deletions(-) delete mode 100644 x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.ts.snap create mode 100644 x-pack/legacy/plugins/uptime/public/hooks/__tests__/__snapshots__/use_url_params.test.tsx.snap delete mode 100644 x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.ts create mode 100644 x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx index f6f1ebcc77952..24ee9e9c7573e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/uptime_date_picker.tsx @@ -21,19 +21,15 @@ interface SuperDateRangePickerRefreshChangedEvent { } interface Props { - history: any; - location: any; refreshApp: () => void; } type UptimeDatePickerProps = Props; export const UptimeDatePicker = (props: UptimeDatePickerProps) => { - const { history, location, refreshApp } = props; - const [ - { autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd }, - updateUrl, - ] = useUrlParams(history, location); + const { refreshApp } = props; + const [getUrlParams, updateUrl] = useUrlParams(); + const { autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd } = getUrlParams(); return ( +
+ {"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-19d","dateRangeEnd":"now-1m","search":"","selectedPingStatus":"down"} +
+ + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.ts b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.ts deleted file mode 100644 index e595c5d58d8d0..0000000000000 --- a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { useUrlParams } from '../use_url_params'; - -describe('useUrlParams', () => { - it('returns the expected params and an update function', () => { - const history: any[] = []; - const location = { pathname: '/', search: '_g=()' }; - const [params, updateFunction] = useUrlParams(history, location); - expect(params).toMatchSnapshot(); - expect(updateFunction).toBeInstanceOf(Function); - }); - - it('returns an update URL function that pushes a new URL to the history object', () => { - const history: any[] = []; - const location = { pathname: '/', search: '_g=()' }; - const [, updateFunction] = useUrlParams(history, location); - const nextPath = updateFunction({ search: 'monitor.id:foo status:down' }); - expect(nextPath).toEqual('/?_g=()&search=monitor.id%3Afoo%20status%3Adown'); - expect(history).toHaveLength(1); - expect(history[0]).toEqual({ - pathname: '/', - search: '_g=()&search=monitor.id%3Afoo%20status%3Adown', - }); - }); -}); diff --git a/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx new file mode 100644 index 0000000000000..0ac8a107f4e33 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/hooks/__tests__/use_url_params.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 { mountWithIntl } from 'test_utils/enzyme_helpers'; +import React, { useState, Fragment } from 'react'; +import { useUrlParams, UptimeUrlParamsHook } from '../use_url_params'; +import { RouteComponentProps } from 'react-router'; +import { UptimeRefreshContext } from '../../contexts'; + +interface MockUrlParamsComponentProps { + hook: UptimeUrlParamsHook; +} + +let mockRouter: RouteComponentProps; + +const UseUrlParamsTestComponent = ({ hook }: MockUrlParamsComponentProps) => { + const [params, setParams] = useState({}); + const [getUrlParams, updateUrlParams] = hook(); + return ( + + {Object.keys(params).length > 0 ?
{JSON.stringify(params)}
: null} + + +
+ ); +}; + +describe('useUrlParams', () => { + beforeEach(() => { + mockRouter = { + // @ts-ignore other properties aren't needed for this test + history: { + push: jest.fn(), + }, + location: { + pathname: '', + search: '?g=""', + state: {}, + hash: '', + }, + match: { + params: '', + isExact: true, + path: '/', + url: 'http://elastic.co', + }, + }; + }); + + it('accepts router props, updates URL params, and returns the current params', () => { + const component = mountWithIntl( + + + + ); + + const setUrlParamsButton = component.find('#setUrlParams'); + setUrlParamsButton.simulate('click'); + + expect(mockRouter.history.push).toHaveBeenCalledWith({ + pathname: '', + search: 'g=%22%22&dateRangeStart=now-12d&dateRangeEnd=now', + }); + }); + + it('gets the expected values using the context', () => { + const component = mountWithIntl( + + + + ); + + const getUrlParamsButton = component.find('#getUrlParams'); + getUrlParamsButton.simulate('click'); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts index 93253454f5ee1..5282f355abc31 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/use_url_params.ts @@ -5,32 +5,44 @@ */ import qs from 'querystring'; +import { useContext } from 'react'; +import { UptimeRefreshContext } from '../contexts'; import { UptimeUrlParams, getSupportedUrlParams } from '../lib/helper'; -interface Location { - pathname: string; - search: string; -} - -export const useUrlParams = ( - history: any, - location: Location -): [UptimeUrlParams, (updatedParams: any) => string] => { - const { pathname, search } = location; - const currentParams: any = qs.parse(search[0] === '?' ? search.slice(1) : search); - - const updateUrl = (updatedParams: any) => { - const updatedSearch = qs.stringify({ - ...currentParams, - ...updatedParams, - }); - history.push({ - pathname, - search: updatedSearch, - }); - - return `${pathname}?${updatedSearch}`; +type GetUrlParams = () => UptimeUrlParams; +type UpdateUrlParams = (updatedParams: { [key: string]: string | number | boolean }) => void; + +export type UptimeUrlParamsHook = () => [GetUrlParams, UpdateUrlParams]; + +export const useUrlParams: UptimeUrlParamsHook = () => { + const refreshContext = useContext(UptimeRefreshContext); + + const getUrlParams: GetUrlParams = () => { + let search: string | undefined; + if (refreshContext.location) { + search = refreshContext.location.search; + } + + const params = search ? { ...qs.parse(search[0] === '?' ? search.slice(1) : search) } : {}; + return getSupportedUrlParams(params); + }; + + const updateUrlParams: UpdateUrlParams = updatedParams => { + if (refreshContext.history && refreshContext.location) { + const { + history, + location: { pathname, search }, + } = refreshContext; + const currentParams: any = qs.parse(search[0] === '?' ? search.slice(1) : search); + history.push({ + pathname, + search: qs.stringify({ + ...currentParams, + ...updatedParams, + }), + }); + } }; - return [getSupportedUrlParams(currentParams), updateUrl]; + return [getUrlParams, updateUrlParams]; }; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap index ce79c549eed2a..26f6ec70865e1 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/__snapshots__/get_supported_url_params.test.ts.snap @@ -1,5 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`getSupportedUrlParams provides defaults for empty string array values 1`] = ` +Object { + "autorefreshInterval": 60000, + "autorefreshIsPaused": false, + "dateRangeEnd": "now", + "dateRangeStart": "now-15m", + "search": "", + "selectedPingStatus": "down", +} +`; + +exports[`getSupportedUrlParams provides defaults for undefined values 1`] = ` +Object { + "autorefreshInterval": 60000, + "autorefreshIsPaused": false, + "dateRangeEnd": "now", + "dateRangeStart": "now-15m", + "search": "", + "selectedPingStatus": "down", +} +`; + exports[`getSupportedUrlParams returns custom values 1`] = ` Object { "autorefreshInterval": 23, @@ -21,3 +43,14 @@ Object { "selectedPingStatus": "down", } `; + +exports[`getSupportedUrlParams returns the first item for string arrays 1`] = ` +Object { + "autorefreshInterval": 60000, + "autorefreshIsPaused": false, + "dateRangeEnd": "now", + "dateRangeStart": "now-18d", + "search": "", + "selectedPingStatus": "down", +} +`; diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts index 0564cedda1fc5..efeff76e47b0c 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/__tests__/get_supported_url_params.test.ts @@ -45,4 +45,25 @@ describe('getSupportedUrlParams', () => { selectedPingStatus: SELECTED_PING_LIST_STATUS, }); }); + + it('returns the first item for string arrays', () => { + const result = getSupportedUrlParams({ + dateRangeStart: ['now-18d', 'now-11d', 'now-5m'], + }); + expect(result).toMatchSnapshot(); + }); + + it('provides defaults for undefined values', () => { + const result = getSupportedUrlParams({ + dateRangeStart: undefined, + }); + expect(result).toMatchSnapshot(); + }); + + it('provides defaults for empty string array values', () => { + const result = getSupportedUrlParams({ + dateRangeStart: [], + }); + expect(result).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts index 567f69df9f8f9..8564f6b94eefd 100644 --- a/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts +++ b/x-pack/legacy/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts @@ -36,9 +36,35 @@ const { SELECTED_PING_LIST_STATUS, } = CLIENT_DEFAULTS; +/** + * Gets the current URL values for the application. If no item is present + * for the URL, a default value is supplied. + * + * @param params A set of key-value pairs where the value is either + * undefined or a string/string array. If a string array is passed, + * only the first item is chosen. Support for lists in the URL will + * require further development. + */ export const getSupportedUrlParams = (params: { - [key: string]: string | undefined; + [key: string]: string | string[] | undefined; }): UptimeUrlParams => { + const filteredParams: { [key: string]: string | undefined } = {}; + Object.keys(params).forEach(key => { + let value: string | undefined; + if (params[key] === undefined) { + value = undefined; + } else if (Array.isArray(params[key])) { + // @ts-ignore this must be an array, and it's ok if the + // 0th element is undefined + value = params[key][0]; + } else { + // @ts-ignore this will not be an array because the preceding + // block tests for that + value = params[key]; + } + filteredParams[key] = value; + }); + const { autorefreshInterval, autorefreshIsPaused, @@ -51,7 +77,7 @@ export const getSupportedUrlParams = (params: { // monitorListSortField, search, selectedPingStatus, - } = params; + } = filteredParams; return { autorefreshInterval: parseUrlInt(autorefreshInterval, AUTOREFRESH_INTERVAL), diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 03b9164cb5af7..0ba44ae8b050a 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -26,7 +26,6 @@ import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { BaseLocationOptions } from '../components/functional/ping_list'; interface MonitorPageProps { - history: { push: any }; location: { pathname: string; search: string }; logMonitorPageLoad: () => void; match: { params: { id: string } }; @@ -38,7 +37,6 @@ interface MonitorPageProps { } export const MonitorPage = ({ - history, location, logMonitorPageLoad, query, @@ -48,7 +46,8 @@ export const MonitorPage = ({ const [monitorId] = useState(decodeURI(parsedPath[0])); const [pingListPageCount, setPingListPageCount] = useState(10); const { colors, refreshApp, setHeadingText } = useContext(UptimeSettingsContext); - const [params, updateUrlParams] = useUrlParams(history, location); + const [getUrlParams, updateUrlParams] = useUrlParams(); + const params = getUrlParams(); const { dateRangeStart, dateRangeEnd, selectedPingStatus } = params; useEffect(() => { diff --git a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx index 936cd4db38d33..e864f1e77f4ab 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/overview.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/overview.tsx @@ -39,17 +39,12 @@ export type UptimeSearchBarQueryChangeHandler = (queryChangedEvent: { queryText?: string; }) => void; -export const OverviewPage = ({ - basePath, - logOverviewPageLoad, - setBreadcrumbs, - history, - location, -}: Props) => { +export const OverviewPage = ({ basePath, logOverviewPageLoad, setBreadcrumbs }: Props) => { const { absoluteStartDate, absoluteEndDate, colors, refreshApp, setHeadingText } = useContext( UptimeSettingsContext ); - const [params, updateUrl] = useUrlParams(history, location); + const [getUrlParams, updateUrl] = useUrlParams(); + const params = getUrlParams(); const { dateRangeStart, dateRangeEnd, diff --git a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx index 4a13538e731fd..188951717dd5a 100644 --- a/x-pack/legacy/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/legacy/plugins/uptime/public/uptime_app.tsx @@ -5,22 +5,7 @@ */ import DateMath from '@elastic/datemath'; -import { - EuiFlexGroup, - EuiFlexItem, - // @ts-ignore missing typings for EuiHeaderLink - EuiHeaderLink, - // @ts-ignore missing typings for EuiHeaderLinks - EuiHeaderLogo, - // @ts-ignore missing typings for EuiHeaderLogo - EuiHeaderSectionItem, - // @ts-ignore missing typings for EuiHeaderSectionItem - EuiPage, - EuiSpacer, - // @ts-ignore missing typings for EuiSuperDatePicker - EuiSuperDatePicker, - EuiTitle, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiSpacer, EuiTitle } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; @@ -32,7 +17,7 @@ import { I18nContext } from 'ui/i18n'; import { UMBreadcrumb } from './breadcrumbs'; import { UMGraphQLClient, UMUpdateBreadcrumbs, UMUpdateBadge } from './lib/lib'; import { MonitorPage, OverviewPage } from './pages'; -import { UptimeRefreshContext, UptimeSettingsContext } from './contexts'; +import { UptimeRefreshContext, UptimeSettingsContext, UMSettingsContextValues } from './contexts'; import { UptimeDatePicker } from './components/functional/uptime_date_picker'; import { useUrlParams } from './hooks'; @@ -119,37 +104,44 @@ const Application = (props: UptimeAppProps) => { setLastRefresh(Date.now()); }; + const [getUrlParams] = useUrlParams(); + const initializeSettingsContextValues = (): UMSettingsContextValues => { + const { + autorefreshInterval, + autorefreshIsPaused, + dateRangeStart, + dateRangeEnd, + } = getUrlParams(); + const absoluteStartDate = DateMath.parse(dateRangeStart); + const absoluteEndDate = DateMath.parse(dateRangeEnd); + return { + // TODO: extract these values to dedicated (and more sensible) constants + absoluteStartDate: absoluteStartDate ? absoluteStartDate.valueOf() : 0, + absoluteEndDate: absoluteEndDate ? absoluteEndDate.valueOf() : 1, + autorefreshInterval, + autorefreshIsPaused, + basePath, + colors, + dateRangeStart, + dateRangeEnd, + isApmAvailable, + isInfraAvailable, + isLogsAvailable, + refreshApp, + setHeadingText, + }; + }; + return ( { - const [ - { autorefreshInterval, autorefreshIsPaused, dateRangeStart, dateRangeEnd }, - ] = useUrlParams(rootRouteProps.history, rootRouteProps.location); - const absoluteStartDate = DateMath.parse(dateRangeStart); - const absoluteEndDate = DateMath.parse(dateRangeEnd); return ( - - + +
{
-
-
+ +
); }} From 980ec8caa3b007638826b113b4247b4e87cd2dd0 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Wed, 24 Jul 2019 17:16:24 +0300 Subject: [PATCH 6/7] [7.x] [telemetry] Analytics Package (#41113) (#41774) --- .github/CODEOWNERS | 72 +++++++++++ packages/kbn-analytics/package.json | 21 ++++ packages/kbn-analytics/src/index.ts | 22 ++++ packages/kbn-analytics/src/metrics/index.ts | 37 ++++++ .../kbn-analytics/src/metrics/stats.ts | 7 +- .../kbn-analytics/src/metrics/ui_stats.ts | 53 ++++++++ packages/kbn-analytics/src/report.ts | 93 ++++++++++++++ packages/kbn-analytics/src/reporter.ts | 114 ++++++++++++++++++ packages/kbn-analytics/src/storage.ts | 38 ++++++ packages/kbn-analytics/src/util.ts | 28 +++++ packages/kbn-analytics/tsconfig.json | 20 +++ .../public/dashboard/lib/migrate_app_state.ts | 4 +- src/legacy/core_plugins/ui_metric/README.md | 29 +++-- src/legacy/core_plugins/ui_metric/index.ts | 23 +++- .../ui_metric/public/hacks/ui_metric_init.ts | 34 ++++++ .../core_plugins/ui_metric/public/index.ts | 38 +----- .../public/services/telemetry_analytics.ts | 60 +++++++++ .../ui_metric/server/routes/api/ui_metric.ts | 69 ++++++++--- .../ui_metric/server/usage/collector.ts | 59 --------- .../apis/ui_metric/ui_metric.js | 39 +++++- .../__tests__/workpad_telemetry.test.tsx | 10 +- .../workpad/workpad_app/workpad_telemetry.tsx | 9 +- .../public/components/element_types/index.js | 4 +- .../fullscreen_control/index.js | 3 +- .../plugins/canvas/public/lib/ui_metric.ts | 13 ++ .../auto_follow_pattern_list.test.js | 4 - .../follower_indices_list.test.js | 4 - .../__jest__/client_integration/home.test.js | 4 - .../auto_follow_pattern_list.js | 4 +- .../auto_follow_pattern_table.js | 4 +- .../follower_indices_table.js | 4 +- .../follower_indices_list.js | 4 +- .../public/app/services/track_ui_metric.js | 10 +- .../index_lifecycle_management/public/app.js | 2 +- .../components/policy_table/policy_table.js | 2 +- .../public/services/api.js | 10 +- .../public/services/ui_metric.js | 7 +- .../public/store/actions/lifecycle.js | 2 +- .../plugins/index_management/public/app.js | 2 +- .../index_list/index_table/index_table.js | 2 +- .../home/templates_list/templates_list.tsx | 4 +- .../index_management/public/services/api.ts | 40 +++--- .../public/services/track_ui_metric.ts | 10 +- .../public/services/use_request.ts | 4 +- .../public/store/reducers/detail_panel.js | 2 +- .../infra/public/hooks/use_track_metric.tsx | 74 ++++++++++++ .../helpers/setup_environment.js | 3 + .../plugins/remote_clusters/public/app/app.js | 4 +- .../remote_cluster_table.js | 4 +- .../public/app/services/api.js | 2 +- .../public/app/services/index.js | 3 +- .../public/app/services/track_ui_metric.js | 26 ---- .../public/app/services/ui_metric.ts | 25 +++- .../app/store/actions/remove_clusters.js | 3 +- .../plugins/remote_clusters/public/plugin.js | 4 +- .../plugins/remote_clusters/public/shim.ts | 4 +- .../job_create_review.test.js | 4 - .../client_integration/job_list.test.js | 4 - .../plugins/rollup/public/crud_app/app.js | 4 +- .../job_list/detail_panel/detail_panel.js | 4 +- .../sections/job_list/job_table/job_table.js | 4 +- .../rollup/public/crud_app/services/index.js | 1 + .../crud_app/services/track_ui_metric.js | 9 +- .../siem/public/components/flyout/index.tsx | 5 +- .../navigation/tab_navigation/index.tsx | 4 +- .../siem/public/lib/track_usage/index.ts | 8 +- .../app/services/ui_metric/ui_metric.ts | 15 ++- .../plugins/snapshot_restore/public/plugin.ts | 2 +- .../plugins/snapshot_restore/public/shim.ts | 6 +- .../plugins/telemetry/common/constants.ts | 6 + x-pack/legacy/plugins/telemetry/index.ts | 7 +- ...telemetry_trigger.js => telemetry_init.ts} | 19 ++- .../public/services/telemetry_opt_in.js | 1 - .../telemetry/server/collectors/index.ts | 1 + .../server/collectors/ui_metric/index.ts} | 7 +- .../telemetry_ui_metric_collector.ts | 45 +++++++ x-pack/package.json | 1 + yarn.lock | 2 +- 78 files changed, 1016 insertions(+), 319 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 packages/kbn-analytics/package.json create mode 100644 packages/kbn-analytics/src/index.ts create mode 100644 packages/kbn-analytics/src/metrics/index.ts rename src/legacy/core_plugins/ui_metric/server/usage/index.ts => packages/kbn-analytics/src/metrics/stats.ts (90%) create mode 100644 packages/kbn-analytics/src/metrics/ui_stats.ts create mode 100644 packages/kbn-analytics/src/report.ts create mode 100644 packages/kbn-analytics/src/reporter.ts create mode 100644 packages/kbn-analytics/src/storage.ts create mode 100644 packages/kbn-analytics/src/util.ts create mode 100644 packages/kbn-analytics/tsconfig.json create mode 100644 src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts create mode 100644 src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts delete mode 100644 src/legacy/core_plugins/ui_metric/server/usage/collector.ts create mode 100644 x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts create mode 100644 x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx delete mode 100644 x-pack/legacy/plugins/remote_clusters/public/app/services/track_ui_metric.js rename x-pack/legacy/plugins/telemetry/public/hacks/{telemetry_trigger.js => telemetry_init.ts} (74%) rename x-pack/legacy/plugins/{canvas/public/lib/ui_metric.js => telemetry/server/collectors/ui_metric/index.ts} (54%) create mode 100644 x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000000..274d9a25ef534 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,72 @@ +# GitHub CODEOWNERS definition +# Identify which groups will be pinged by changes to different parts of the codebase. +# For more info, see https://help.github.com/articles/about-codeowners/ + +# App Architecture +/src/plugins/data/ @elastic/kibana-app-arch +/src/plugins/kibana_utils/ @elastic/kibana-app-arch + +# APM +/x-pack/legacy/plugins/apm/ @elastic/apm-ui + +# Beats +/x-pack/legacy/plugins/beats_management/ @elastic/beats + +# Canvas +/x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas + +# Code +/x-pack/legacy/plugins/code/ @teams/code +/x-pack/test/functional/apps/code/ @teams/code +/x-pack/test/api_integration/apis/code/ @teams/code + +# Infrastructure and Logs UI +/x-pack/legacy/plugins/infra/ @elastic/infra-logs-ui + +# Machine Learning +/x-pack/legacy/plugins/ml/ @elastic/ml-ui + +# Operations +/renovate.json5 @elastic/kibana-operations +/src/dev/ @elastic/kibana-operations +/src/setup_node_env/ @elastic/kibana-operations +/src/optimize/ @elastic/kibana-operations + +# Platform +/src/core/ @elastic/kibana-platform +/src/legacy/server/saved_objects/ @elastic/kibana-platform +/src/legacy/ui/public/saved_objects @elastic/kibana-platform + +# Security +/x-pack/legacy/plugins/security/ @elastic/kibana-security +/x-pack/legacy/plugins/spaces/ @elastic/kibana-security +/x-pack/legacy/plugins/encrypted_saved_objects/ @elastic/kibana-security +/src/legacy/server/csp/ @elastic/kibana-security +/x-pack/plugins/security/ @elastic/kibana-security + +# Kibana Stack Services +/packages/kbn-analytics/ @elastic/kibana-stack-services +/src/legacy/core_plugins/ui_metric/ @elastic/kibana-stack-services +/x-pack/legacy/plugins/telemetry @elastic/kibana-stack-services +/x-pack/legacy/plugins/alerting @elastic/kibana-stack-services +/x-pack/legacy/plugins/actions @elastic/kibana-stack-services +/x-pack/legacy/plugins/task_manager @elastic/kibana-stack-services + +# Design +**/*.scss @elastic/kibana-design + +# Elasticsearch UI +/src/legacy/core_plugins/console/ @elastic/es-ui +/x-pack/legacy/plugins/console_extensions/ @elastic/es-ui +/x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui +/x-pack/legacy/plugins/index_lifecycle_management/ @elastic/es-ui +/x-pack/legacy/plugins/index_management/ @elastic/es-ui +/x-pack/legacy/plugins/license_management/ @elastic/es-ui +/x-pack/legacy/plugins/remote_clusters/ @elastic/es-ui +/x-pack/legacy/plugins/rollup/ @elastic/es-ui +/x-pack/legacy/plugins/searchprofiler/ @elastic/es-ui +/x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui +/x-pack/legacy/plugins/watcher/ @elastic/es-ui + +# Kibana TSVB external contractors +/src/legacy/core_plugins/metrics/ @elastic/kibana-tsvb-external diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json new file mode 100644 index 0000000000000..e8fb1aea81a72 --- /dev/null +++ b/packages/kbn-analytics/package.json @@ -0,0 +1,21 @@ +{ + "name": "@kbn/analytics", + "private": true, + "version": "1.0.0", + "description": "Kibana Analytics tool", + "main": "target/index.js", + "types": "target/index.d.ts", + "author": "Ahmad Bamieh ", + "license": "Apache-2.0", + "scripts": { + "build": "tsc", + "kbn:bootstrap": "yarn build", + "kbn:watch": "yarn build --watch" + }, + "devDependencies": { + "typescript": "3.5.1" + }, + "dependencies": { + "@kbn/dev-utils": "1.0.0" + } +} diff --git a/packages/kbn-analytics/src/index.ts b/packages/kbn-analytics/src/index.ts new file mode 100644 index 0000000000000..63fd115fa7594 --- /dev/null +++ b/packages/kbn-analytics/src/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 { createReporter, ReportHTTP, Reporter, ReporterConfig } from './reporter'; +export { UiStatsMetricType, METRIC_TYPE } from './metrics'; +export { Report, ReportManager } from './report'; diff --git a/packages/kbn-analytics/src/metrics/index.ts b/packages/kbn-analytics/src/metrics/index.ts new file mode 100644 index 0000000000000..13b9e5dc59e4e --- /dev/null +++ b/packages/kbn-analytics/src/metrics/index.ts @@ -0,0 +1,37 @@ +/* + * 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 { UiStatsMetric, UiStatsMetricType } from './ui_stats'; + +export { + UiStatsMetric, + createUiStatsMetric, + UiStatsMetricReport, + UiStatsMetricType, +} from './ui_stats'; +export { Stats } from './stats'; + +export type Metric = UiStatsMetric; +export type MetricType = keyof typeof METRIC_TYPE; + +export enum METRIC_TYPE { + COUNT = 'count', + LOADED = 'loaded', + CLICK = 'click', +} diff --git a/src/legacy/core_plugins/ui_metric/server/usage/index.ts b/packages/kbn-analytics/src/metrics/stats.ts similarity index 90% rename from src/legacy/core_plugins/ui_metric/server/usage/index.ts rename to packages/kbn-analytics/src/metrics/stats.ts index b3b88b94d1269..993290167018c 100644 --- a/src/legacy/core_plugins/ui_metric/server/usage/index.ts +++ b/packages/kbn-analytics/src/metrics/stats.ts @@ -17,4 +17,9 @@ * under the License. */ -export { registerUiMetricUsageCollector } from './collector'; +export interface Stats { + min: number; + max: number; + sum: number; + avg: number; +} diff --git a/packages/kbn-analytics/src/metrics/ui_stats.ts b/packages/kbn-analytics/src/metrics/ui_stats.ts new file mode 100644 index 0000000000000..7615fd20645e2 --- /dev/null +++ b/packages/kbn-analytics/src/metrics/ui_stats.ts @@ -0,0 +1,53 @@ +/* + * 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 { Stats } from './stats'; +import { METRIC_TYPE } from './'; + +export type UiStatsMetricType = METRIC_TYPE.CLICK | METRIC_TYPE.LOADED | METRIC_TYPE.COUNT; +export interface UiStatsMetricConfig { + type: T; + appName: string; + eventName: string; + count?: number; +} + +export interface UiStatsMetric { + type: T; + appName: string; + eventName: string; + count: number; +} + +export function createUiStatsMetric({ + type, + appName, + eventName, + count = 1, +}: UiStatsMetricConfig): UiStatsMetric { + return { type, appName, eventName, count }; +} + +export interface UiStatsMetricReport { + key: string; + appName: string; + eventName: string; + type: UiStatsMetricType; + stats: Stats; +} diff --git a/packages/kbn-analytics/src/report.ts b/packages/kbn-analytics/src/report.ts new file mode 100644 index 0000000000000..6187455fa60a5 --- /dev/null +++ b/packages/kbn-analytics/src/report.ts @@ -0,0 +1,93 @@ +/* + * 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 { UnreachableCaseError } from './util'; +import { Metric, Stats, UiStatsMetricReport, METRIC_TYPE } from './metrics'; + +export interface Report { + uiStatsMetrics: { + [key: string]: UiStatsMetricReport; + }; +} + +export class ReportManager { + public report: Report; + constructor(report?: Report) { + this.report = report || ReportManager.createReport(); + } + static createReport() { + return { uiStatsMetrics: {} }; + } + public clearReport() { + this.report = ReportManager.createReport(); + } + public isReportEmpty(): boolean { + return Object.keys(this.report.uiStatsMetrics).length === 0; + } + private incrementStats(count: number, stats?: Stats): Stats { + const { min = 0, max = 0, sum = 0 } = stats || {}; + const newMin = Math.min(min, count); + const newMax = Math.max(max, count); + const newAvg = newMin + newMax / 2; + const newSum = sum + count; + + return { + min: newMin, + max: newMax, + avg: newAvg, + sum: newSum, + }; + } + assignReports(newMetrics: Metric[]) { + newMetrics.forEach(newMetric => this.assignReport(this.report, newMetric)); + } + static createMetricKey(metric: Metric): string { + switch (metric.type) { + case METRIC_TYPE.CLICK: + case METRIC_TYPE.LOADED: + case METRIC_TYPE.COUNT: { + const { appName, type, eventName } = metric; + return `${appName}-${type}-${eventName}`; + } + default: + throw new UnreachableCaseError(metric.type); + } + } + private assignReport(report: Report, metric: Metric) { + switch (metric.type) { + case METRIC_TYPE.CLICK: + case METRIC_TYPE.LOADED: + case METRIC_TYPE.COUNT: { + const { appName, type, eventName, count } = metric; + const key = ReportManager.createMetricKey(metric); + const existingStats = (report.uiStatsMetrics[key] || {}).stats; + this.report.uiStatsMetrics[key] = { + key, + appName, + eventName, + type, + stats: this.incrementStats(count, existingStats), + }; + return; + } + default: + throw new UnreachableCaseError(metric.type); + } + } +} diff --git a/packages/kbn-analytics/src/reporter.ts b/packages/kbn-analytics/src/reporter.ts new file mode 100644 index 0000000000000..37d23aa443090 --- /dev/null +++ b/packages/kbn-analytics/src/reporter.ts @@ -0,0 +1,114 @@ +/* + * 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 { wrapArray } from './util'; +import { Metric, UiStatsMetric, createUiStatsMetric } from './metrics'; + +import { Storage, ReportStorageManager } from './storage'; +import { Report, ReportManager } from './report'; + +export interface ReporterConfig { + http: ReportHTTP; + storage?: Storage; + checkInterval?: number; + debug?: boolean; + storageKey?: string; +} + +export type ReportHTTP = (report: Report) => Promise; + +export class Reporter { + checkInterval: number; + private interval: any; + private http: ReportHTTP; + private reportManager: ReportManager; + private storageManager: ReportStorageManager; + private debug: boolean; + + constructor(config: ReporterConfig) { + const { http, storage, debug, checkInterval = 10000, storageKey = 'analytics' } = config; + + this.http = http; + this.checkInterval = checkInterval; + this.interval = null; + this.storageManager = new ReportStorageManager(storageKey, storage); + const storedReport = this.storageManager.get(); + this.reportManager = new ReportManager(storedReport); + this.debug = !!debug; + } + + private saveToReport(newMetrics: Metric[]) { + this.reportManager.assignReports(newMetrics); + this.storageManager.store(this.reportManager.report); + } + + private flushReport() { + this.reportManager.clearReport(); + this.storageManager.store(this.reportManager.report); + } + + public start() { + if (!this.interval) { + this.interval = setTimeout(() => { + this.interval = null; + this.sendReports(); + }, this.checkInterval); + } + } + + private log(message: any) { + if (this.debug) { + // eslint-disable-next-line + console.debug(message); + } + } + + public reportUiStats( + appName: string, + type: UiStatsMetric['type'], + eventNames: string | string[], + count?: number + ) { + const metrics = wrapArray(eventNames).map(eventName => { + if (this) this.log(`${type} Metric -> (${appName}:${eventName}):`); + const report = createUiStatsMetric({ type, appName, eventName, count }); + this.log(report); + return report; + }); + this.saveToReport(metrics); + } + + public async sendReports() { + if (!this.reportManager.isReportEmpty()) { + try { + await this.http(this.reportManager.report); + this.flushReport(); + } catch (err) { + this.log(`Error Sending Metrics Report ${err}`); + } + } + this.start(); + } +} + +export function createReporter(reportedConf: ReporterConfig) { + const reporter = new Reporter(reportedConf); + reporter.start(); + return reporter; +} diff --git a/packages/kbn-analytics/src/storage.ts b/packages/kbn-analytics/src/storage.ts new file mode 100644 index 0000000000000..9abf9fa7dac2c --- /dev/null +++ b/packages/kbn-analytics/src/storage.ts @@ -0,0 +1,38 @@ +/* + * 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 { Report } from './report'; + +export type Storage = Map; +export class ReportStorageManager { + storageKey: string; + private storage?: Storage; + constructor(storageKey: string, storage?: Storage) { + this.storageKey = storageKey; + this.storage = storage; + } + public get(): Report | undefined { + if (!this.storage) return; + return this.storage.get(this.storageKey); + } + public store(report: Report) { + if (!this.storage) return; + this.storage.set(this.storageKey, report); + } +} diff --git a/packages/kbn-analytics/src/util.ts b/packages/kbn-analytics/src/util.ts new file mode 100644 index 0000000000000..fe3e8b8f9f7ba --- /dev/null +++ b/packages/kbn-analytics/src/util.ts @@ -0,0 +1,28 @@ +/* + * 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 function wrapArray(subj: T | T[]): T[] { + return Array.isArray(subj) ? subj : [subj]; +} + +export class UnreachableCaseError extends Error { + constructor(val: never) { + super(`Unreachable case: ${val}`); + } +} diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json new file mode 100644 index 0000000000000..fcb8ddbbde68e --- /dev/null +++ b/packages/kbn-analytics/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationDir": "./target", + "outDir": "./target", + "stripInternal": true, + "declarationMap": true, + "types": [ + "jest", + "node" + ] + }, + "include": [ + "./src/**/*.ts" + ], + "exclude": [ + "target" + ] +} diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts index e97719aed6bc3..9bd93029f06d8 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/migrate_app_state.ts @@ -20,7 +20,7 @@ import semver from 'semver'; import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; -import { trackUiMetric } from '../../../../ui_metric/public'; +import { createUiStatsReporter, METRIC_TYPE } from '../../../../ui_metric/public'; import { DashboardAppState, SavedDashboardPanelTo60, @@ -59,7 +59,7 @@ export function migrateAppState(appState: { [key: string]: unknown } | Dashboard const version = (panel as SavedDashboardPanel730ToLatest).version; // This will help us figure out when to remove support for older style URLs. - trackUiMetric('DashboardPanelVersionInUrl', `${version}`); + createUiStatsReporter('DashboardPanelVersionInUrl')(METRIC_TYPE.LOADED, `${version}`); return semver.satisfies(version, '<7.3'); }); diff --git a/src/legacy/core_plugins/ui_metric/README.md b/src/legacy/core_plugins/ui_metric/README.md index 9b78cf600dc4e..90855faff61a6 100644 --- a/src/legacy/core_plugins/ui_metric/README.md +++ b/src/legacy/core_plugins/ui_metric/README.md @@ -16,29 +16,32 @@ the name of a dashboard they've viewed, or the timestamp of the interaction. ## How to use it -To track a user interaction, import the `trackUiMetric` helper function from UI Metric app: +To track a user interaction, import the `createUiStatsReporter` helper function from UI Metric app: ```js -import { trackUiMetric } from 'relative/path/to/src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter, METRIC_TYPE } from 'relative/path/to/src/legacy/core_plugins/ui_metric/public'; +const trackMetric = createUiStatsReporter(``); +trackMetric(METRIC_TYPE.CLICK, ``); +trackMetric('click', ``); ``` +Metric Types: + - `METRIC_TYPE.CLICK` for tracking clicks `trackMetric(METRIC_TYPE.CLICK, 'my_button_clicked');` + - `METRIC_TYPE.LOADED` for a component load or page load `trackMetric(METRIC_TYPE.LOADED', 'my_component_loaded');` + - `METRIC_TYPE.COUNT` for a tracking a misc count `trackMetric(METRIC_TYPE.COUNT', 'my_counter', });` + Call this function whenever you would like to track a user interaction within your app. The function -accepts two arguments, `appName` and `metricType`. These should be underscore-delimited strings. -For example, to track the `my_metric` metric in the app `my_app` call `trackUiMetric('my_app', 'my_metric)`. +accepts two arguments, `metricType` and `eventNames`. These should be underscore-delimited strings. +For example, to track the `my_event` metric in the app `my_app` call `trackUiMetric(METRIC_TYPE.*, 'my_event)`. That's all you need to do! -To track multiple metrics within a single request, provide an array of metric types, e.g. `trackUiMetric('my_app', ['my_metric1', 'my_metric2', 'my_metric3'])`. - -**NOTE:** When called, this function sends a `POST` request to `/api/ui_metric/{appName}/{metricType}`. -It's important that this request is sent via the `trackUiMetric` function, because it contains special -logic for blocking the request if the user hasn't opted in to telemetry. +To track multiple metrics within a single request, provide an array of events, e.g. `trackMetric(METRIC_TYPE.*, ['my_event1', 'my_event2', 'my_event3'])`. ### Disallowed characters -The colon and comma characters (`,`, `:`) should not be used in app name or metric types. Colons play -a sepcial role in how metrics are stored as saved objects, and the API endpoint uses commas to delimit -multiple metric types in a single API request. +The colon character (`:`) should not be used in app name or event names. Colons play +a special role in how metrics are stored as saved objects. ### Tracking timed interactions @@ -47,7 +50,7 @@ logic yourself. You'll also need to predefine some buckets into which the UI met For example, if you're timing how long it takes to create a visualization, you may decide to measure interactions that take less than 1 minute, 1-5 minutes, 5-20 minutes, and longer than 20 minutes. To track these interactions, you'd use the timed length of the interaction to determine whether to -use a `metricType` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`. +use a `eventName` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`. ## How it works diff --git a/src/legacy/core_plugins/ui_metric/index.ts b/src/legacy/core_plugins/ui_metric/index.ts index 34ca346b8756e..6c957f23b5c40 100644 --- a/src/legacy/core_plugins/ui_metric/index.ts +++ b/src/legacy/core_plugins/ui_metric/index.ts @@ -18,9 +18,10 @@ */ import { resolve } from 'path'; +import JoiNamespace from 'joi'; +import { Server } from 'hapi'; import { Legacy } from '../../../../kibana'; -import { registerUserActionRoute } from './server/routes/api/ui_metric'; -import { registerUiMetricUsageCollector } from './server/usage/index'; +import { registerUiMetricRoute } from './server/routes/api/ui_metric'; // eslint-disable-next-line import/no-default-export export default function(kibana: any) { @@ -28,15 +29,25 @@ export default function(kibana: any) { id: 'ui_metric', require: ['kibana', 'elasticsearch'], publicDir: resolve(__dirname, 'public'), - + config(Joi: typeof JoiNamespace) { + return Joi.object({ + enabled: Joi.boolean().default(true), + debug: Joi.boolean().default(Joi.ref('$dev')), + }).default(); + }, uiExports: { + injectDefaultVars(server: Server) { + const config = server.config(); + return { + debugUiMetric: config.get('ui_metric.debug'), + }; + }, mappings: require('./mappings.json'), - hacks: ['plugins/ui_metric'], + hacks: ['plugins/ui_metric/hacks/ui_metric_init'], }, init(server: Legacy.Server) { - registerUserActionRoute(server); - registerUiMetricUsageCollector(server); + registerUiMetricRoute(server); }, }); } diff --git a/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts b/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts new file mode 100644 index 0000000000000..7aafc82cfe4c6 --- /dev/null +++ b/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +// @ts-ignore +import { uiModules } from 'ui/modules'; +import chrome from 'ui/chrome'; +import { createAnalyticsReporter, setTelemetryReporter } from '../services/telemetry_analytics'; + +function telemetryInit($injector: any) { + const localStorage = $injector.get('localStorage'); + const debug = chrome.getInjected('debugUiMetric'); + const $http = $injector.get('$http'); + const basePath = chrome.getBasePath(); + const uiReporter = createAnalyticsReporter({ localStorage, $http, basePath, debug }); + setTelemetryReporter(uiReporter); +} + +uiModules.get('kibana').run(telemetryInit); diff --git a/src/legacy/core_plugins/ui_metric/public/index.ts b/src/legacy/core_plugins/ui_metric/public/index.ts index 6bf54943a3ac4..b1e78b56d05d0 100644 --- a/src/legacy/core_plugins/ui_metric/public/index.ts +++ b/src/legacy/core_plugins/ui_metric/public/index.ts @@ -17,39 +17,5 @@ * under the License. */ -import chrome from 'ui/chrome'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { getCanTrackUiMetrics } from 'ui/ui_metric'; -import { API_BASE_PATH } from '../common'; - -let _http: any; - -uiModules.get('kibana').run(($http: any) => { - _http = $http; -}); - -function createErrorMessage(subject: string): any { - const message = - `trackUiMetric was called with ${subject}, which is not allowed to contain a colon. ` + - `Colons play a special role in how metrics are saved as stored objects`; - return new Error(message); -} - -export function trackUiMetric(appName: string, metricType: string | string[]) { - if (!getCanTrackUiMetrics()) { - return; - } - - if (appName.includes(':')) { - throw createErrorMessage(`app name '${appName}'`); - } - - if (metricType.includes(':')) { - throw createErrorMessage(`metric type ${metricType}`); - } - - const metricTypes = Array.isArray(metricType) ? metricType.join(',') : metricType; - const uri = chrome.addBasePath(`${API_BASE_PATH}/${appName}/${metricTypes}`); - _http.post(uri); -} +export { createUiStatsReporter } from './services/telemetry_analytics'; +export { METRIC_TYPE } from '@kbn/analytics'; diff --git a/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts b/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts new file mode 100644 index 0000000000000..63adccb3e02b0 --- /dev/null +++ b/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts @@ -0,0 +1,60 @@ +/* + * 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 { createReporter, Reporter, UiStatsMetricType } from '@kbn/analytics'; + +let telemetryReporter: Reporter; + +export const setTelemetryReporter = (aTelemetryReporter: Reporter): void => { + telemetryReporter = aTelemetryReporter; +}; + +export const getTelemetryReporter = () => { + return telemetryReporter; +}; + +export const createUiStatsReporter = (appName: string) => ( + type: UiStatsMetricType, + eventNames: string | string[], + count?: number +): void => { + if (telemetryReporter) { + return telemetryReporter.reportUiStats(appName, type, eventNames, count); + } +}; + +interface AnalyicsReporterConfig { + localStorage: any; + basePath: string; + debug: boolean; + $http: ng.IHttpService; +} + +export function createAnalyticsReporter(config: AnalyicsReporterConfig) { + const { localStorage, basePath, $http, debug } = config; + + return createReporter({ + debug, + storage: localStorage, + async http(report) { + const url = `${basePath}/api/telemetry/report`; + await $http.post(url, { report }); + }, + }); +} diff --git a/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts b/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts index 4bbafbb18f550..8a7950c46fa31 100644 --- a/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts +++ b/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts @@ -17,36 +17,65 @@ * under the License. */ +import Joi from 'joi'; import Boom from 'boom'; +import { Report } from '@kbn/analytics'; import { Server } from 'hapi'; -import { API_BASE_PATH } from '../../../common'; -export const registerUserActionRoute = (server: Server) => { - /* - * Increment a count on an object representing a specific interaction with the UI. - */ +export async function storeReport(server: any, report: Report) { + const { getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const internalRepository = getSavedObjectsRepository(callWithInternalUser); + + const metricKeys = Object.keys(report.uiStatsMetrics); + return Promise.all( + metricKeys.map(async key => { + const metric = report.uiStatsMetrics[key]; + const { appName, eventName } = metric; + const savedObjectId = `${appName}:${eventName}`; + return internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'); + }) + ); +} + +export function registerUiMetricRoute(server: Server) { server.route({ - path: `${API_BASE_PATH}/{appName}/{metricTypes}`, method: 'POST', - handler: async (request: any) => { - const { appName, metricTypes } = request.params; + path: '/api/telemetry/report', + options: { + validate: { + payload: Joi.object({ + report: Joi.object({ + uiStatsMetrics: Joi.object() + .pattern( + /.*/, + Joi.object({ + key: Joi.string().required(), + type: Joi.string().required(), + appName: Joi.string().required(), + eventName: Joi.string().required(), + stats: Joi.object({ + min: Joi.number(), + sum: Joi.number(), + max: Joi.number(), + avg: Joi.number(), + }).allow(null), + }) + ) + .allow(null), + }), + }), + }, + }, + handler: async (req: any, h: any) => { + const { report } = req.payload; try { - const { getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - - const incrementRequests = metricTypes.split(',').map((metricType: string) => { - const savedObjectId = `${appName}:${metricType}`; - // This object is created if it doesn't already exist. - return internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'); - }); - - await Promise.all(incrementRequests); + await storeReport(server, report); return {}; } catch (error) { return new Boom('Something went wrong', { statusCode: error.status }); } }, }); -}; +} diff --git a/src/legacy/core_plugins/ui_metric/server/usage/collector.ts b/src/legacy/core_plugins/ui_metric/server/usage/collector.ts deleted file mode 100644 index bbb7b1af8e7c7..0000000000000 --- a/src/legacy/core_plugins/ui_metric/server/usage/collector.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -const UI_METRIC_USAGE_TYPE = 'ui_metric'; - -export function registerUiMetricUsageCollector(server: any) { - const collector = server.usage.collectorSet.makeUsageCollector({ - type: UI_METRIC_USAGE_TYPE, - fetch: async (callCluster: any) => { - const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - const savedObjectsClient = new SavedObjectsClient(internalRepository); - - const { saved_objects: rawUiMetrics } = await savedObjectsClient.find({ - type: 'ui-metric', - fields: ['count'], - }); - - const uiMetricsByAppName = rawUiMetrics.reduce((accum: any, rawUiMetric: any) => { - const { - id, - attributes: { count }, - } = rawUiMetric; - - const [appName, metricType] = id.split(':'); - - if (!accum[appName]) { - accum[appName] = []; - } - - const pair = { key: metricType, value: count }; - accum[appName].push(pair); - return accum; - }, {}); - - return uiMetricsByAppName; - }, - isReady: () => true, - }); - - server.usage.collectorSet.register(collector); -} diff --git a/test/api_integration/apis/ui_metric/ui_metric.js b/test/api_integration/apis/ui_metric/ui_metric.js index e841176025bb5..efa6be47b50c9 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.js +++ b/test/api_integration/apis/ui_metric/ui_metric.js @@ -18,16 +18,33 @@ */ import expect from '@kbn/expect'; +import { ReportManager } from '@kbn/analytics'; export default function ({ getService }) { const supertest = getService('supertest'); const es = getService('es'); + const createMetric = (eventName) => ({ + key: ReportManager.createMetricKey({ appName: 'myApp', type: 'click', eventName }), + eventName, + appName: 'myApp', + type: 'click', + stats: { sum: 1, avg: 1, min: 1, max: 1 }, + }); + describe('ui_metric API', () => { + const uiStatsMetric = createMetric('myEvent'); + const report = { + uiStatsMetrics: { + [uiStatsMetric.key]: uiStatsMetric, + } + }; it('increments the count field in the document defined by the {app}/{action_type} path', async () => { await supertest - .post('/api/ui_metric/myApp/myAction') + .post('/api/telemetry/report') .set('kbn-xsrf', 'kibana') + .set('content-type', 'application/json') + .send({ report }) .expect(200); return es.search({ @@ -35,14 +52,24 @@ export default function ({ getService }) { q: 'type:user-action', }).then(response => { const ids = response.hits.hits.map(({ _id }) => _id); - expect(ids.includes('user-action:myApp:myAction')); + expect(ids.includes('user-action:myApp:myEvent')); }); }); - it('supports comma-delimited action types', async () => { + it('supports multiple events', async () => { + const uiStatsMetric1 = createMetric('myEvent1'); + const uiStatsMetric2 = createMetric('myEvent2'); + const report = { + uiStatsMetrics: { + [uiStatsMetric1.key]: uiStatsMetric1, + [uiStatsMetric2.key]: uiStatsMetric2, + } + }; await supertest - .post('/api/ui_metric/myApp/myAction1,myAction2') + .post('/api/telemetry/report') .set('kbn-xsrf', 'kibana') + .set('content-type', 'application/json') + .send({ report }) .expect(200); return es.search({ @@ -50,8 +77,8 @@ export default function ({ getService }) { q: 'type:user-action', }).then(response => { const ids = response.hits.hits.map(({ _id }) => _id); - expect(ids.includes('user-action:myApp:myAction1')); - expect(ids.includes('user-action:myApp:myAction2')); + expect(ids.includes('user-action:myApp:myEvent1')); + expect(ids.includes('user-action:myApp:myEvent2')); }); }); }); diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx index db1427d6a7201..b8779e7d44fcf 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx @@ -11,6 +11,7 @@ import { WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric, } from '../workpad_telemetry'; +import { METRIC_TYPE } from '../../../../lib/ui_metric'; const trackMetric = jest.fn(); const Component = withUnconnectedElementsLoadedTelemetry(() =>
, trackMetric); @@ -83,7 +84,7 @@ describe('Elements Loaded Telemetry', () => { /> ); - expect(trackMetric).toBeCalledWith(WorkpadLoadedMetric); + expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, WorkpadLoadedMetric); }); it('only tracks loaded once', () => { @@ -154,7 +155,10 @@ describe('Elements Loaded Telemetry', () => { /> ); - expect(trackMetric).toBeCalledWith([WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]); + expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, [ + WorkpadLoadedMetric, + WorkpadLoadedWithErrorsMetric, + ]); }); it('tracks when the workpad changes and is loaded', () => { @@ -198,7 +202,7 @@ describe('Elements Loaded Telemetry', () => { /> ); - expect(trackMetric).toBeCalledWith(WorkpadLoadedMetric); + expect(trackMetric).toBeCalledWith(METRIC_TYPE.LOADED, WorkpadLoadedMetric); }); it('does not track if workpad has no elements', () => { diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx index 59375d3d0ef00..720f1726c1e3c 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx @@ -7,7 +7,7 @@ import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; // @ts-ignore: Local Untyped -import { trackCanvasUiMetric } from '../../../lib/ui_metric'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric'; // @ts-ignore: Local Untyped import { getElementCounts } from '../../../state/selectors/workpad'; // @ts-ignore: Local Untyped @@ -79,7 +79,7 @@ function areAllElementsInResolvedArgs(workpad: Workpad, resolvedArgs: ResolvedAr export const withUnconnectedElementsLoadedTelemetry = function

( Component: React.ComponentType

, - trackMetric: (metric: string | string[]) => void = trackCanvasUiMetric + trackMetric = trackCanvasUiMetric ): React.SFC

{ return function ElementsLoadedTelemetry( props: P & ElementsLoadedTelemetryProps @@ -117,11 +117,10 @@ export const withUnconnectedElementsLoadedTelemetry = function

resolvedArgsAreForWorkpad ) { if (telemetryElementCounts.error > 0) { - trackMetric([WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]); + trackMetric(METRIC_TYPE.LOADED, [WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric]); } else { - trackMetric(WorkpadLoadedMetric); + trackMetric(METRIC_TYPE.LOADED, WorkpadLoadedMetric); } - setHasReported(true); } }); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js b/x-pack/legacy/plugins/canvas/public/components/element_types/index.js index 557e1e3883c54..20d19d2fcbd82 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/element_types/index.js @@ -15,7 +15,7 @@ import { notify } from '../../lib/notify'; import { selectToplevelNodes } from '../../state/actions/transient'; import { insertNodes, addElement } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric } from '../../lib/ui_metric'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; import { ElementTypes as Component } from './element_types'; const customElementAdded = 'elements-custom-added'; @@ -51,7 +51,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { selectToplevelNodes(clonedNodes); // then select the cloned node(s) } onClose(); - trackCanvasUiMetric(customElementAdded); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); }, // custom element search findCustomElements: async text => { diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js index aba8905127729..cf2e80cee03bf 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js @@ -16,7 +16,7 @@ import { getPages, getWorkpad, } from '../../../state/selectors/workpad'; -import { trackCanvasUiMetric } from '../../../lib/ui_metric'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric'; import { LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY, @@ -54,6 +54,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { if (value === true) { trackCanvasUiMetric( + METRIC_TYPE.COUNT, stateProps.autoplayEnabled ? [LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY] : LAUNCHED_FULLSCREEN diff --git a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts new file mode 100644 index 0000000000000..33976a147df46 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; + +export const trackCanvasUiMetric = createUiStatsReporter('canvas'); +export { METRIC_TYPE }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js index f116e5657280a..167d0799d8c48 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -19,10 +19,6 @@ jest.mock('ui/index_patterns', () => { return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE }; }); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - const { setup } = pageHelpers.autoFollowPatternList; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js index 8df920aa06423..f3047206c77ba 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js @@ -19,10 +19,6 @@ jest.mock('ui/index_patterns', () => { return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE }; }); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - const { setup } = pageHelpers.followerIndexList; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js index a4b559dc24d2f..cf6ccd80f461f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js @@ -28,10 +28,6 @@ jest.mock('ui/index_patterns', () => { return { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE }; }); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - const { setup } = pageHelpers.home; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js index 12311f5f6d2dc..d73fca1d76dcf 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js @@ -19,7 +19,7 @@ import { import routing from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; -import { trackUiMetric } from '../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants'; import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components'; import { AutoFollowPatternTable, DetailPanel } from './components'; @@ -60,7 +60,7 @@ export class AutoFollowPatternList extends PureComponent { componentDidMount() { const { loadAutoFollowPatterns, loadAutoFollowStats, selectAutoFollowPattern, history } = this.props; - trackUiMetric(UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD); loadAutoFollowPatterns(); loadAutoFollowStats(); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 8d38d7efec4f6..dee85c54653b1 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -20,7 +20,7 @@ import { import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK } from '../../../../../constants'; import { AutoFollowPatternDeleteProvider } from '../../../../../components'; import routing from '../../../../../services/routing'; -import { trackUiMetric } from '../../../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric'; export class AutoFollowPatternTable extends PureComponent { static propTypes = { @@ -77,7 +77,7 @@ export class AutoFollowPatternTable extends PureComponent { return ( { - trackUiMetric(UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); + trackUiMetric(METRIC_TYPE.CLICK, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); selectAutoFollowPattern(name); }} data-test-subj="autoFollowPatternLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index 19fe01c417423..620dd4e79c7ed 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -23,7 +23,7 @@ import { FollowerIndexUnfollowProvider } from '../../../../../components'; import routing from '../../../../../services/routing'; -import { trackUiMetric } from '../../../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric'; import { ContextMenu } from '../context_menu'; export class FollowerIndicesTable extends PureComponent { @@ -191,7 +191,7 @@ export class FollowerIndicesTable extends PureComponent { return ( { - trackUiMetric(UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); + trackUiMetric(METRIC_TYPE.CLICK, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); selectFollowerIndex(name); }} data-test-subj="followerIndexLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js index 5c593458dd50b..b78f6399168a2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js @@ -19,7 +19,7 @@ import { import routing from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; -import { trackUiMetric } from '../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants'; import { SectionLoading, SectionError, SectionUnauthorized } from '../../../components'; import { FollowerIndicesTable, DetailPanel } from './components'; @@ -58,7 +58,7 @@ export class FollowerIndicesList extends PureComponent { componentDidMount() { const { loadFollowerIndices, selectFollowerIndex, history } = this.props; - trackUiMetric(UIM_FOLLOWER_INDEX_LIST_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_FOLLOWER_INDEX_LIST_LOAD); loadFollowerIndices(); // Select the pattern in the URL query params diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js index 7b2f0d0407862..23314fdd0ef83 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trackUiMetric as track } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter, METRIC_TYPE } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; import { UIM_APP_NAME } from '../constants'; -export function trackUiMetric(actionType) { - track(UIM_APP_NAME, actionType); -} - +export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); +export { METRIC_TYPE }; /** * Transparently return provided request Promise, while allowing us to track * a successful completion of the request. @@ -18,7 +16,7 @@ export function trackUiMetric(actionType) { export function trackUserRequest(request, actionType) { // Only track successful actions. return request.then(response => { - trackUiMetric(actionType); + trackUiMetric(METRIC_TYPE.LOADED, actionType); // We return the response immediately without waiting for the tracking request to resolve, // to avoid adding additional latency. return response; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/app.js b/x-pack/legacy/plugins/index_lifecycle_management/public/app.js index d9381d98f17ad..ddd3ac2f68f8c 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/app.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/app.js @@ -12,7 +12,7 @@ import { PolicyTable } from './sections/policy_table'; import { trackUiMetric } from './services'; export const App = () => { - useEffect(() => trackUiMetric(UIM_APP_LOAD), []); + useEffect(() => trackUiMetric('loaded', UIM_APP_LOAD), []); return ( diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js index 8d8d9700cbc27..25141e5a2fe58 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/sections/policy_table/components/policy_table/policy_table.js @@ -178,7 +178,7 @@ export class PolicyTable extends Component { className="policyTable__link" data-test-subj="policyTablePolicyNameLink" href={getPolicyPath(value)} - onClick={() => trackUiMetric(UIM_EDIT_CLICK)} + onClick={() => trackUiMetric('click', UIM_EDIT_CLICK)} > {value} diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/services/api.js b/x-pack/legacy/plugins/index_lifecycle_management/public/services/api.js index f76a30761d34b..3819822088e97 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/services/api.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/services/api.js @@ -58,34 +58,34 @@ export async function savePolicy(policy, httpClient = getHttpClient()) { export async function deletePolicy(policyName, httpClient = getHttpClient()) { const response = await httpClient.delete(`${apiPrefix}/policies/${encodeURIComponent(policyName)}`); // Only track successful actions. - trackUiMetric(UIM_POLICY_DELETE); + trackUiMetric('count', UIM_POLICY_DELETE); return response.data; } export const retryLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => { const response = await httpClient.post(`${apiPrefix}/index/retry`, { indexNames }); // Only track successful actions. - trackUiMetric(UIM_INDEX_RETRY_STEP); + trackUiMetric('count', UIM_INDEX_RETRY_STEP); return response.data; }; export const removeLifecycleForIndex = async (indexNames, httpClient = getHttpClient()) => { const response = await httpClient.post(`${apiPrefix}/index/remove`, { indexNames }); // Only track successful actions. - trackUiMetric(UIM_POLICY_DETACH_INDEX); + trackUiMetric('count', UIM_POLICY_DETACH_INDEX); return response.data; }; export const addLifecyclePolicyToIndex = async (body, httpClient = getHttpClient()) => { const response = await httpClient.post(`${apiPrefix}/index/add`, body); // Only track successful actions. - trackUiMetric(UIM_POLICY_ATTACH_INDEX); + trackUiMetric('count', UIM_POLICY_ATTACH_INDEX); return response.data; }; export const addLifecyclePolicyToTemplate = async (body, httpClient = getHttpClient()) => { const response = await httpClient.post(`${apiPrefix}/template`, body); // Only track successful actions. - trackUiMetric(UIM_POLICY_ATTACH_INDEX_TEMPLATE); + trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE); return response.data; }; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.js b/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.js index 8382d16596d8f..d33d6688f4922 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.js @@ -5,8 +5,7 @@ */ import { get } from 'lodash'; - -import { trackUiMetric as track } from '../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter } from '../../../../../../src/legacy/core_plugins/ui_metric/public'; import { UIM_APP_NAME, @@ -29,9 +28,7 @@ import { defaultHotPhase, } from '../store/defaults'; -export function trackUiMetric(metricType) { - track(UIM_APP_NAME, metricType); -} +export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); export function getUiMetricsForPhases(phases) { const phaseUiMetrics = [{ diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/store/actions/lifecycle.js b/x-pack/legacy/plugins/index_lifecycle_management/public/store/actions/lifecycle.js index 9acb76d0936e1..64d73e4aaeff5 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/store/actions/lifecycle.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/store/actions/lifecycle.js @@ -32,7 +32,7 @@ export const saveLifecyclePolicy = (lifecycle, isNew) => async () => { const uiMetrics = getUiMetricsForPhases(lifecycle.phases); uiMetrics.push(isNew ? UIM_POLICY_CREATE : UIM_POLICY_UPDATE); - trackUiMetric(uiMetrics); + trackUiMetric('count', uiMetrics); const message = i18n.translate('xpack.indexLifecycleMgmt.editPolicy.successfulSaveMessage', { diff --git a/x-pack/legacy/plugins/index_management/public/app.js b/x-pack/legacy/plugins/index_management/public/app.js index c911759932e49..8f25183a21bc1 100644 --- a/x-pack/legacy/plugins/index_management/public/app.js +++ b/x-pack/legacy/plugins/index_management/public/app.js @@ -11,7 +11,7 @@ import { IndexManagementHome } from './sections/home'; import { trackUiMetric } from './services'; export const App = () => { - useEffect(() => trackUiMetric(UIM_APP_LOAD), []); + useEffect(() => trackUiMetric('loaded', UIM_APP_LOAD), []); return ( diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/index_list/index_table/index_table.js b/x-pack/legacy/plugins/index_management/public/sections/home/index_list/index_table/index_table.js index 708cd1eb8867a..ddd3c14dba696 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/index_list/index_table/index_table.js +++ b/x-pack/legacy/plugins/index_management/public/sections/home/index_list/index_table/index_table.js @@ -220,7 +220,7 @@ export class IndexTable extends Component { className="indTable__link" data-test-subj="indexTableIndexNameLink" onClick={() => { - trackUiMetric(UIM_SHOW_DETAILS_CLICK); + trackUiMetric('click', UIM_SHOW_DETAILS_CLICK); openDetailPanel(value); }} > diff --git a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx index e5b5e9ad281e8..a17bfaf1ce27a 100644 --- a/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx +++ b/x-pack/legacy/plugins/index_management/public/sections/home/templates_list/templates_list.tsx @@ -19,7 +19,7 @@ import { SectionError, SectionLoading } from '../../../components'; import { TemplatesTable } from './templates_table'; import { loadIndexTemplates } from '../../../services/api'; import { Template } from '../../../../common/types'; -import { trackUiMetric } from '../../../services/track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { UIM_TEMPLATE_LIST_LOAD } from '../../../../common/constants'; export const TemplatesList: React.FunctionComponent = () => { @@ -38,7 +38,7 @@ export const TemplatesList: React.FunctionComponent = () => { // Track component loaded useEffect(() => { - trackUiMetric(UIM_TEMPLATE_LIST_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_TEMPLATE_LIST_LOAD); }, []); if (isLoading) { diff --git a/x-pack/legacy/plugins/index_management/public/services/api.ts b/x-pack/legacy/plugins/index_management/public/services/api.ts index 0b16d02a733b0..3a6c9fdf7b238 100644 --- a/x-pack/legacy/plugins/index_management/public/services/api.ts +++ b/x-pack/legacy/plugins/index_management/public/services/api.ts @@ -32,7 +32,7 @@ import { import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants'; -import { trackUiMetric } from './track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from './track_ui_metric'; import { useRequest, sendRequest } from './use_request'; import { Template } from '../../common/types'; @@ -67,8 +67,8 @@ export async function closeIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/close`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_CLOSE_MANY : UIM_INDEX_CLOSE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_CLOSE_MANY : UIM_INDEX_CLOSE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -78,8 +78,8 @@ export async function deleteIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/delete`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_DELETE_MANY : UIM_INDEX_DELETE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_DELETE_MANY : UIM_INDEX_DELETE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -89,8 +89,8 @@ export async function openIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/open`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_OPEN_MANY : UIM_INDEX_OPEN; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_OPEN_MANY : UIM_INDEX_OPEN; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -100,8 +100,8 @@ export async function refreshIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/refresh`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_REFRESH_MANY : UIM_INDEX_REFRESH; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_REFRESH_MANY : UIM_INDEX_REFRESH; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -111,8 +111,8 @@ export async function flushIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/flush`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_FLUSH_MANY : UIM_INDEX_FLUSH; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_FLUSH_MANY : UIM_INDEX_FLUSH; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -123,8 +123,8 @@ export async function forcemergeIndices(indices: string[], maxNumSegments: strin }; const response = await httpClient.post(`${apiPrefix}/indices/forcemerge`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_FORCE_MERGE_MANY : UIM_INDEX_FORCE_MERGE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_FORCE_MERGE_MANY : UIM_INDEX_FORCE_MERGE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -134,8 +134,8 @@ export async function clearCacheIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/clear_cache`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_CLEAR_CACHE_MANY : UIM_INDEX_CLEAR_CACHE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_CLEAR_CACHE_MANY : UIM_INDEX_CLEAR_CACHE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } export async function freezeIndices(indices: string[]) { @@ -144,8 +144,8 @@ export async function freezeIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/freeze`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_FREEZE_MANY : UIM_INDEX_FREEZE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_FREEZE_MANY : UIM_INDEX_FREEZE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } export async function unfreezeIndices(indices: string[]) { @@ -154,8 +154,8 @@ export async function unfreezeIndices(indices: string[]) { }; const response = await httpClient.post(`${apiPrefix}/indices/unfreeze`, body); // Only track successful requests. - const actionType = indices.length > 1 ? UIM_INDEX_UNFREEZE_MANY : UIM_INDEX_UNFREEZE; - trackUiMetric(actionType); + const eventName = indices.length > 1 ? UIM_INDEX_UNFREEZE_MANY : UIM_INDEX_UNFREEZE; + trackUiMetric(METRIC_TYPE.COUNT, eventName); return response.data; } @@ -167,7 +167,7 @@ export async function loadIndexSettings(indexName: string) { export async function updateIndexSettings(indexName: string, settings: object) { const response = await httpClient.put(`${apiPrefix}/settings/${indexName}`, settings); // Only track successful requests. - trackUiMetric(UIM_UPDATE_SETTINGS); + trackUiMetric(METRIC_TYPE.COUNT, UIM_UPDATE_SETTINGS); return response; } diff --git a/x-pack/legacy/plugins/index_management/public/services/track_ui_metric.ts b/x-pack/legacy/plugins/index_management/public/services/track_ui_metric.ts index 6378fa0aa413b..cf5b04d513bc4 100644 --- a/x-pack/legacy/plugins/index_management/public/services/track_ui_metric.ts +++ b/x-pack/legacy/plugins/index_management/public/services/track_ui_metric.ts @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trackUiMetric as track } from '../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; import { UIM_APP_NAME } from '../../common/constants'; -export function trackUiMetric(metricType: string) { - track(UIM_APP_NAME, metricType); -} +export { METRIC_TYPE }; +export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); diff --git a/x-pack/legacy/plugins/index_management/public/services/use_request.ts b/x-pack/legacy/plugins/index_management/public/services/use_request.ts index 4d275ec124c43..2168ec4f655df 100644 --- a/x-pack/legacy/plugins/index_management/public/services/use_request.ts +++ b/x-pack/legacy/plugins/index_management/public/services/use_request.ts @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; import { getHttpClient } from './api'; -import { trackUiMetric } from './track_ui_metric'; +import { trackUiMetric, METRIC_TYPE } from './track_ui_metric'; interface SendRequest { path?: string; @@ -35,7 +35,7 @@ export const sendRequest = async ({ // Track successful request if (uimActionType) { - trackUiMetric(uimActionType); + trackUiMetric(METRIC_TYPE.COUNT, uimActionType); } return { diff --git a/x-pack/legacy/plugins/index_management/public/store/reducers/detail_panel.js b/x-pack/legacy/plugins/index_management/public/store/reducers/detail_panel.js index 47949f1751854..6a3df9d38ee60 100644 --- a/x-pack/legacy/plugins/index_management/public/store/reducers/detail_panel.js +++ b/x-pack/legacy/plugins/index_management/public/store/reducers/detail_panel.js @@ -54,7 +54,7 @@ export const detailPanel = handleActions( }; if (panelTypeToUiMetricMap[panelType]) { - trackUiMetric(panelTypeToUiMetricMap[panelType]); + trackUiMetric('count', panelTypeToUiMetricMap[panelType]); } return { diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx new file mode 100644 index 0000000000000..54359cf0aa692 --- /dev/null +++ b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect } from 'react'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; + +/** + * Note: The UI Metric plugin will take care of sending this data to the telemetry server. + * You can find these metrics stored at: + * stack_stats.kibana.plugins.ui_metric.{app}.{metric}(__delayed_{n}ms)? + * which will be an array of objects each containing a key, representing the metric, and + * a value, which will be a counter + */ + +type ObservabilityApp = 'infra_metrics' | 'infra_logs' | 'apm' | 'uptime'; + +const trackerCache = new Map>(); + +function getTrackerForApp(app: string) { + const cached = trackerCache.get(app); + if (cached) { + return cached; + } + + const tracker = createUiStatsReporter(app); + trackerCache.set(app, tracker); + + return tracker; +} + +interface TrackOptions { + app: ObservabilityApp; + metricType?: METRIC_TYPE; + delay?: number; // in ms +} +type EffectDeps = unknown[]; + +type TrackMetricOptions = TrackOptions & { metric: string }; + +export { METRIC_TYPE }; +export function useTrackMetric( + { app, metric, metricType = METRIC_TYPE.COUNT, delay = 0 }: TrackMetricOptions, + effectDependencies: EffectDeps = [] +) { + useEffect(() => { + let decoratedMetric = metric; + if (delay > 0) { + decoratedMetric += `__delayed_${delay}ms`; + } + const trackUiMetric = getTrackerForApp(app); + const id = setTimeout(() => trackUiMetric(metricType, decoratedMetric), Math.max(delay, 0)); + return () => clearTimeout(id); + }, effectDependencies); +} + +/** + * useTrackPageview is a convenience wrapper for tracking a pageview + * Its metrics will be found at: + * stack_stats.kibana.plugins.ui_metric.{app}.pageview__{path}(__delayed_{n}ms)? + */ +type TrackPageviewProps = TrackOptions & { path: string }; + +export function useTrackPageview( + { path, ...rest }: TrackPageviewProps, + effectDependencies: EffectDeps = [] +) { + useTrackMetric({ ...rest, metric: `pageview__${path}` }, effectDependencies); +} diff --git a/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js b/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js index ba02da248ce12..c3bf8668af8dc 100644 --- a/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js @@ -13,7 +13,9 @@ import { fatalError, toastNotifications } from 'ui/notify'; // eslint-disable-li import { init as initBreadcrumb } from '../../../public/app/services/breadcrumb'; import { init as initHttp } from '../../../public/app/services/http'; import { init as initNotification } from '../../../public/app/services/notification'; +import { init as initUiMetric } from '../../../public/app/services/ui_metric'; import { init as initHttpRequests } from './http_requests'; +import { createUiStatsReporter } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; export const setupEnvironment = () => { chrome.breadcrumbs = { @@ -23,6 +25,7 @@ export const setupEnvironment = () => { initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path); initBreadcrumb(() => {}, MANAGEMENT_BREADCRUMB); initNotification(toastNotifications, fatalError); + initUiMetric(createUiStatsReporter); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/app.js b/x-pack/legacy/plugins/remote_clusters/public/app/app.js index 4062f6e476b8f..6fea66ad03c9c 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/app.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/app.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; import { Switch, Route, Redirect } from 'react-router-dom'; import { CRUD_APP_BASE_PATH, UIM_APP_LOAD } from './constants'; -import { registerRouter, setUserHasLeftApp, trackUiMetric } from './services'; +import { registerRouter, setUserHasLeftApp, trackUiMetric, METRIC_TYPE } from './services'; import { RemoteClusterList, RemoteClusterAdd, RemoteClusterEdit } from './sections'; export class App extends Component { @@ -34,7 +34,7 @@ export class App extends Component { } componentDidMount() { - trackUiMetric(UIM_APP_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD); } componentWillUnmount() { diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js index 6677d8f0c6fbd..9c128ed6f744a 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { CRUD_APP_BASE_PATH, UIM_SHOW_DETAILS_CLICK } from '../../../constants'; -import { getRouterLinkProps, trackUiMetric } from '../../../services'; +import { getRouterLinkProps, trackUiMetric, METRIC_TYPE } from '../../../services'; import { ConnectionStatus, RemoveClusterButtonProvider } from '../components'; export class RemoteClusterTable extends Component { @@ -91,7 +91,7 @@ export class RemoteClusterTable extends Component { { - trackUiMetric(UIM_SHOW_DETAILS_CLICK); + trackUiMetric(METRIC_TYPE.CLICK, UIM_SHOW_DETAILS_CLICK); openDetailPanel(name); }} > diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/api.js b/x-pack/legacy/plugins/remote_clusters/public/app/services/api.js index 77a26221f7be7..d7df62eef95ee 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/api.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/services/api.js @@ -5,7 +5,7 @@ */ import { UIM_CLUSTER_ADD, UIM_CLUSTER_UPDATE } from '../constants'; -import { trackUserRequest } from './track_ui_metric'; +import { trackUserRequest } from './ui_metric'; import { sendGet, sendPost, sendPut, sendDelete } from './http'; export async function loadClusters() { diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/index.js b/x-pack/legacy/plugins/remote_clusters/public/app/services/index.js index 51308b4c0788e..218576cc5195b 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/index.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/services/index.js @@ -40,4 +40,5 @@ export { export { trackUiMetric, -} from './track_ui_metric'; + METRIC_TYPE, +} from './ui_metric'; diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/track_ui_metric.js b/x-pack/legacy/plugins/remote_clusters/public/app/services/track_ui_metric.js deleted file mode 100644 index 7b2f0d0407862..0000000000000 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/track_ui_metric.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { trackUiMetric as track } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; -import { UIM_APP_NAME } from '../constants'; - -export function trackUiMetric(actionType) { - track(UIM_APP_NAME, actionType); -} - -/** - * Transparently return provided request Promise, while allowing us to track - * a successful completion of the request. - */ -export function trackUserRequest(request, actionType) { - // Only track successful actions. - return request.then(response => { - trackUiMetric(actionType); - // We return the response immediately without waiting for the tracking request to resolve, - // to avoid adding additional latency. - return response; - }); -} diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/services/ui_metric.ts b/x-pack/legacy/plugins/remote_clusters/public/app/services/ui_metric.ts index 16f09ee109738..36a23476c1873 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/services/ui_metric.ts +++ b/x-pack/legacy/plugins/remote_clusters/public/app/services/ui_metric.ts @@ -5,13 +5,28 @@ */ import { UIM_APP_NAME } from '../constants'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; -export let track: any; +export let trackUiMetric: ReturnType; +export { METRIC_TYPE }; -export function init(_track: any): void { - track = _track; +export function init(getReporter: typeof createUiStatsReporter): void { + trackUiMetric = getReporter(UIM_APP_NAME); } -export function trackUiMetric(actionType: string): any { - return track(UIM_APP_NAME, actionType); +/** + * Transparently return provided request Promise, while allowing us to track + * a successful completion of the request. + */ +export function trackUserRequest(request: Promise, eventName: string) { + // Only track successful actions. + return request.then((response: any) => { + trackUiMetric(METRIC_TYPE.COUNT, eventName); + // We return the response immediately without waiting for the tracking request to resolve, + // to avoid adding additional latency. + return response; + }); } diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js b/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js index c9cbc377f9e29..e85a40e8ed289 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/store/actions/remove_clusters.js @@ -12,6 +12,7 @@ import { UIM_CLUSTER_REMOVE, UIM_CLUSTER_REMOVE_MANY } from '../../constants'; import { removeClusterRequest as sendRemoveClusterRequest, trackUiMetric, + METRIC_TYPE, } from '../../services'; import { @@ -83,7 +84,7 @@ export const removeClusters = (names) => async (dispatch, getState) => { if (itemsDeleted.length > 0) { // Only track successful requests. - trackUiMetric(names.length > 1 ? UIM_CLUSTER_REMOVE_MANY : UIM_CLUSTER_REMOVE); + trackUiMetric(METRIC_TYPE.COUNT, names.length > 1 ? UIM_CLUSTER_REMOVE_MANY : UIM_CLUSTER_REMOVE); if (itemsDeleted.length === 1) { toasts.addSuccess(i18n.translate('xpack.remoteClusters.removeAction.successSingleNotificationTitle', { diff --git a/x-pack/legacy/plugins/remote_clusters/public/plugin.js b/x-pack/legacy/plugins/remote_clusters/public/plugin.js index 3ea7c8c13d458..c370bf7243d1e 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/plugin.js +++ b/x-pack/legacy/plugins/remote_clusters/public/plugin.js @@ -35,7 +35,7 @@ export class Plugin { if (getInjectedVar('remoteClustersUiEnabled')) { const { management: { getSection, breadcrumb: managementBreadcrumb }, - uiMetric: { track }, + uiMetric: { createUiStatsReporter }, } = pluginsStart; const esSection = getSection('elasticsearch'); @@ -49,7 +49,7 @@ export class Plugin { // Initialize services initBreadcrumbs(setBreadcrumbs, managementBreadcrumb); initDocumentation(`${elasticWebsiteUrl}guide/en/elasticsearch/reference/${docLinkVersion}/`); - initUiMetric(track); + initUiMetric(createUiStatsReporter); initNotification(toasts, fatalError); const unmountReactApp = () => { diff --git a/x-pack/legacy/plugins/remote_clusters/public/shim.ts b/x-pack/legacy/plugins/remote_clusters/public/shim.ts index 213624d8e0d58..83975fa4bd0fe 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/shim.ts +++ b/x-pack/legacy/plugins/remote_clusters/public/shim.ts @@ -9,7 +9,7 @@ import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; import { fatalError } from 'ui/notify'; import { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } from 'ui/documentation_links'; -import { trackUiMetric as track } from '../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter } from '../../../../../src/legacy/core_plugins/ui_metric/public'; export function createShim() { const { @@ -35,7 +35,7 @@ export function createShim() { breadcrumb: MANAGEMENT_BREADCRUMB, }, uiMetric: { - track, + createUiStatsReporter, }, }, }; diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js index aa6ff1325c117..7acbbf800d63d 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_create_review.test.js @@ -19,10 +19,6 @@ jest.mock('ui/chrome', () => ({ jest.mock('lodash/function/debounce', () => fn => fn); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - const { setup } = pageHelpers.jobCreate; describe('Create Rollup Job, step 5: Metrics', () => { diff --git a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js index 5a629c094a632..09aa13b9fefe4 100644 --- a/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js +++ b/x-pack/legacy/plugins/rollup/__jest__/client_integration/job_list.test.js @@ -28,10 +28,6 @@ jest.mock('ui/chrome', () => ({ } })); -jest.mock('../../../../../../src/legacy/core_plugins/ui_metric/public', () => ({ - trackUiMetric: jest.fn(), -})); - jest.mock('../../public/crud_app/services', () => { const services = require.requireActual('../../public/crud_app/services'); return { diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/app.js b/x-pack/legacy/plugins/rollup/public/crud_app/app.js index e039bf1d40fcc..0e42194097492 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/app.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/app.js @@ -10,7 +10,7 @@ import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; import { UIM_APP_LOAD } from '../../common'; import { CRUD_APP_BASE_PATH } from './constants'; -import { registerRouter, setUserHasLeftApp, trackUiMetric } from './services'; +import { registerRouter, setUserHasLeftApp, trackUiMetric, METRIC_TYPE } from './services'; import { JobList, JobCreate } from './sections'; class ShareRouter extends Component { @@ -41,7 +41,7 @@ class ShareRouter extends Component { export class App extends Component { // eslint-disable-line react/no-multi-comp componentDidMount() { - trackUiMetric(UIM_APP_LOAD); + trackUiMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD); } componentWillUnmount() { diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js index 445b436c123d2..1b9d77dd128ab 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.js @@ -33,7 +33,7 @@ import { UIM_DETAIL_PANEL_METRICS_TAB_CLICK, UIM_DETAIL_PANEL_JSON_TAB_CLICK, } from '../../../../../common'; -import { trackUiMetric } from '../../../services'; +import { trackUiMetric, METRIC_TYPE } from '../../../services'; import { JobActionMenu, @@ -114,7 +114,7 @@ export class DetailPanelUi extends Component { renderedTabs.push( { - trackUiMetric(tabToUiMetricMap[tab]); + trackUiMetric(METRIC_TYPE.CLICK, tabToUiMetricMap[tab]); openDetailPanel({ panelType: tab, jobId: id }); }} isSelected={isSelected} diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js index 21d8a5a984ee8..e1c885156faa6 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.js @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { UIM_SHOW_DETAILS_CLICK } from '../../../../../common'; -import { trackUiMetric } from '../../../services'; +import { trackUiMetric, METRIC_TYPE } from '../../../services'; import { JobActionMenu, JobStatus } from '../../components'; const COLUMNS = [{ @@ -259,7 +259,7 @@ export class JobTableUi extends Component { content = ( { - trackUiMetric(UIM_SHOW_DETAILS_CLICK); + trackUiMetric(METRIC_TYPE.CLICK, UIM_SHOW_DETAILS_CLICK); openDetailPanel(job.id); }} > diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js index 7cc1a0abfdd3c..fdff4ebc89733 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/services/index.js @@ -95,4 +95,5 @@ export { export { trackUiMetric, + METRIC_TYPE, } from './track_ui_metric'; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js b/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js index efb97e05a05e6..de4d43b3f8c1a 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/services/track_ui_metric.js @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trackUiMetric as track } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter, METRIC_TYPE } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; import { UIM_APP_NAME } from '../../../common'; -export function trackUiMetric(actionType) { - track(UIM_APP_NAME, actionType); -} +export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); +export { METRIC_TYPE }; /** * Transparently return provided request Promise, while allowing us to track @@ -18,7 +17,7 @@ export function trackUiMetric(actionType) { export function trackUserRequest(request, actionType) { // Only track successful actions. return request.then(response => { - trackUiMetric(actionType); + trackUiMetric(METRIC_TYPE.LOADED, actionType); // We return the response immediately without waiting for the tracking request to resolve, // to avoid adding additional latency. return response; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx index 531894b5e487d..ac999b4287fcc 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/index.tsx @@ -14,12 +14,11 @@ import { ActionCreator } from 'typescript-fsa'; import { State, timelineSelectors } from '../../store'; import { DataProvider } from '../timeline/data_providers/data_provider'; - import { FlyoutButton } from './button'; import { Pane } from './pane'; import { timelineActions } from '../../store/actions'; import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/helpers'; -import { trackUiAction as track } from '../../lib/track_usage'; +import { trackUiAction as track, METRIC_TYPE } from '../../lib/track_usage'; /** The height in pixels of the flyout header, exported for use in height calculations */ export const flyoutHeaderHeight: number = 60; @@ -100,7 +99,7 @@ export const FlyoutComponent = pure( show={!show} timelineId={timelineId} onOpen={() => { - track('open_timeline'); + track(METRIC_TYPE.LOADED, 'open_timeline'); showTimeline!({ id: timelineId, show: true }); }} /> diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx index 823f41065fbb8..94f6dcb22a637 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import styled from 'styled-components'; import { getHostsUrl, getNetworkUrl, getOverviewUrl, getTimelinesUrl } from '../../link_to'; -import { trackUiAction as track } from '../../../lib/track_usage'; +import { trackUiAction as track, METRIC_TYPE } from '../../../lib/track_usage'; import * as i18n from '../translations'; @@ -101,7 +101,7 @@ export class TabNavigation extends React.PureComponent { - track(`tab_${tab.id}`); + track(METRIC_TYPE.CLICK, `tab_${tab.id}`); }} > {tab.name} diff --git a/x-pack/legacy/plugins/siem/public/lib/track_usage/index.ts b/x-pack/legacy/plugins/siem/public/lib/track_usage/index.ts index b2ebbaa2baea6..934e5c441e076 100644 --- a/x-pack/legacy/plugins/siem/public/lib/track_usage/index.ts +++ b/x-pack/legacy/plugins/siem/public/lib/track_usage/index.ts @@ -5,7 +5,11 @@ */ // @ts-ignore -import { trackUiMetric } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; import { APP_ID } from '../../../common/constants'; -export const trackUiAction = (metricType: string) => trackUiMetric(APP_ID, metricType); +export const trackUiAction = createUiStatsReporter(APP_ID); +export { METRIC_TYPE }; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/ui_metric/ui_metric.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/ui_metric/ui_metric.ts index 1f06ff83e4ee0..a2f0a6e1a5482 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/ui_metric/ui_metric.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/ui_metric/ui_metric.ts @@ -4,16 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ import { UIM_APP_NAME } from '../../constants'; +import { + createUiStatsReporter, + METRIC_TYPE, +} from '../../../../../../../../src/legacy/core_plugins/ui_metric/public'; class UiMetricService { - public track: any = () => {}; + track?: ReturnType; - public init = (track: any): void => { - this.track = track; + public init = (getReporter: typeof createUiStatsReporter): void => { + this.track = getReporter(UIM_APP_NAME); }; - public trackUiMetric = (actionType: string): any => { - return this.track(UIM_APP_NAME, actionType); + public trackUiMetric = (eventName: string): void => { + if (!this.track) throw Error('UiMetricService not initialized.'); + return this.track(METRIC_TYPE.COUNT, eventName); }; } diff --git a/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts b/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts index b3fce61d3876a..f590237bec737 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/plugin.ts @@ -39,7 +39,7 @@ export class Plugin { textService.init(i18n); breadcrumbService.init(chrome, management.constants.BREADCRUMB); documentationLinksService.init(documentation.esDocBasePath, documentation.esPluginDocBasePath); - uiMetricService.init(uiMetric.track); + uiMetricService.init(uiMetric.createUiStatsReporter); const unmountReactApp = (): void => { const elem = document.getElementById(REACT_ROOT_ID); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/shim.ts b/x-pack/legacy/plugins/snapshot_restore/public/shim.ts index 45083d997fdb6..77604f90fd570 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/shim.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/shim.ts @@ -16,7 +16,7 @@ import routes from 'ui/routes'; import { HashRouter } from 'react-router-dom'; // @ts-ignore: allow traversal to fail on x-pack build -import { trackUiMetric as track } from '../../../../../src/legacy/core_plugins/ui_metric/public'; +import { createUiStatsReporter } from '../../../../../src/legacy/core_plugins/ui_metric/public'; export interface AppCore { i18n: { @@ -63,7 +63,7 @@ export interface Plugins extends AppPlugins { }; }; uiMetric: { - track: typeof track; + createUiStatsReporter: typeof createUiStatsReporter; }; } @@ -118,7 +118,7 @@ export function createShim(): { core: Core; plugins: Plugins } { }, }, uiMetric: { - track, + createUiStatsReporter, }, }, }; diff --git a/x-pack/legacy/plugins/telemetry/common/constants.ts b/x-pack/legacy/plugins/telemetry/common/constants.ts index 8c6cfb6e558da..c50f36ac94497 100644 --- a/x-pack/legacy/plugins/telemetry/common/constants.ts +++ b/x-pack/legacy/plugins/telemetry/common/constants.ts @@ -81,3 +81,9 @@ export const KIBANA_LOCALIZATION_STATS_TYPE = 'localization'; * @type {string} */ export const TELEMETRY_QUERY_SOURCE = 'TELEMETRY'; + +/** + * UI metric usage type + * @type {string} + */ +export const UI_METRIC_USAGE_TYPE = 'ui_metric'; diff --git a/x-pack/legacy/plugins/telemetry/index.ts b/x-pack/legacy/plugins/telemetry/index.ts index 4125dae82f9fa..6d4e9be67fb98 100644 --- a/x-pack/legacy/plugins/telemetry/index.ts +++ b/x-pack/legacy/plugins/telemetry/index.ts @@ -17,6 +17,7 @@ import { telemetryPlugin } from './server'; import { createLocalizationUsageCollector, createTelemetryUsageCollector, + createUiMetricUsageCollector, } from './server/collectors'; const ENDPOINT_VERSION = 'v2'; @@ -72,10 +73,7 @@ export const telemetry = (kibana: any) => { activeSpace: null, }; }, - hacks: [ - 'plugins/telemetry/hacks/telemetry_opt_in', - 'plugins/telemetry/hacks/telemetry_trigger', - ], + hacks: ['plugins/telemetry/hacks/telemetry_init', 'plugins/telemetry/hacks/telemetry_opt_in'], mappings, }, init(server: Server) { @@ -89,6 +87,7 @@ export const telemetry = (kibana: any) => { // register collectors server.usage.collectorSet.register(createLocalizationUsageCollector(server)); server.usage.collectorSet.register(createTelemetryUsageCollector(server)); + server.usage.collectorSet.register(createUiMetricUsageCollector(server)); // expose server.expose('telemetryCollectionInterval', REPORT_INTERVAL_MS); diff --git a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_trigger.js b/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts similarity index 74% rename from x-pack/legacy/plugins/telemetry/public/hacks/telemetry_trigger.js rename to x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts index efee9e07dc7bd..c44da2b36bf1e 100644 --- a/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_trigger.js +++ b/x-pack/legacy/plugins/telemetry/public/hacks/telemetry_init.ts @@ -4,24 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore import { uiModules } from 'ui/modules'; +// @ts-ignore import { Path } from 'plugins/xpack_main/services/path'; +// @ts-ignore +import { npStart } from 'ui/new_platform'; +// @ts-ignore import { Telemetry } from './telemetry'; +// @ts-ignore import { fetchTelemetry } from './fetch_telemetry'; -import { npStart } from 'ui/new_platform'; -function telemetryStart($injector) { +function telemetryInit($injector: any) { + const $http = $injector.get('$http'); + const telemetryEnabled = npStart.core.injectedMetadata.getInjectedVar('telemetryEnabled'); if (telemetryEnabled) { // no telemetry for non-logged in users - if (Path.isUnauthenticated()) { return; } + if (Path.isUnauthenticated()) { + return; + } - const $http = $injector.get('$http'); const sender = new Telemetry($injector, () => fetchTelemetry($http)); - sender.start(); } } -uiModules.get('telemetry/hacks').run(telemetryStart); +uiModules.get('telemetry/hacks').run(telemetryInit); diff --git a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.js b/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.js index a1b45b3fa91da..2fcd2012a1528 100644 --- a/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.js +++ b/x-pack/legacy/plugins/telemetry/public/services/telemetry_opt_in.js @@ -17,7 +17,6 @@ export function TelemetryOptInProvider($injector, chrome) { getOptIn: () => currentOptInStatus, setOptIn: async (enabled) => { setCanTrackUiMetrics(enabled); - const $http = $injector.get('$http'); try { diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/index.ts b/x-pack/legacy/plugins/telemetry/server/collectors/index.ts index 2123d5a9251a7..c9b94a8ea5d5e 100644 --- a/x-pack/legacy/plugins/telemetry/server/collectors/index.ts +++ b/x-pack/legacy/plugins/telemetry/server/collectors/index.ts @@ -11,4 +11,5 @@ export { getLocalStats } from './local'; export { getStats } from './get_stats'; export { encryptTelemetry } from './encryption'; export { createTelemetryUsageCollector } from './usage'; +export { createUiMetricUsageCollector } from './ui_metric'; export { createLocalizationUsageCollector } from './localization'; diff --git a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.js b/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts similarity index 54% rename from x-pack/legacy/plugins/canvas/public/lib/ui_metric.js rename to x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts index 57ec5b03356c9..f5a49587d49c8 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.js +++ b/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/index.ts @@ -4,9 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { trackUiMetric } from '../../../../../../src/legacy/core_plugins/ui_metric/public'; - -const APP = 'canvas'; -export const trackCanvasUiMetric = uiMetrics => { - trackUiMetric(APP, uiMetrics); -}; +export { createUiMetricUsageCollector } from './telemetry_ui_metric_collector'; diff --git a/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts new file mode 100644 index 0000000000000..a931400399b44 --- /dev/null +++ b/x-pack/legacy/plugins/telemetry/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -0,0 +1,45 @@ +/* + * 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 { UI_METRIC_USAGE_TYPE } from '../../../common/constants'; + +export function createUiMetricUsageCollector(server: any) { + const { collectorSet } = server.usage; + return collectorSet.makeUsageCollector({ + type: UI_METRIC_USAGE_TYPE, + fetch: async () => { + const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const internalRepository = getSavedObjectsRepository(callWithInternalUser); + const savedObjectsClient = new SavedObjectsClient(internalRepository); + + const { saved_objects: rawUiMetrics } = await savedObjectsClient.find({ + type: 'ui-metric', + fields: ['count'], + }); + + const uiMetricsByAppName = rawUiMetrics.reduce((accum: any, rawUiMetric: any) => { + const { + id, + attributes: { count }, + } = rawUiMetric; + + const [appName, metricType] = id.split(':'); + + if (!accum[appName]) { + accum[appName] = []; + } + + const pair = { key: metricType, value: count }; + accum[appName].push(pair); + return accum; + }, {}); + + return uiMetricsByAppName; + }, + isReady: () => true, + }); +} diff --git a/x-pack/package.json b/x-pack/package.json index 6c5379c28d2f3..2c8fcdeb06762 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -178,6 +178,7 @@ "@elastic/nodegit": "0.25.0-alpha.22", "@elastic/numeral": "2.3.3", "@elastic/request-crypto": "^1.0.2", + "@kbn/analytics": "1.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/elastic-idx": "1.0.0", diff --git a/yarn.lock b/yarn.lock index db20e2c06009f..31320716262d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27796,7 +27796,7 @@ typescript-fsa@^2.0.0, typescript-fsa@^2.5.0: resolved "https://registry.yarnpkg.com/typescript-fsa/-/typescript-fsa-2.5.0.tgz#1baec01b5e8f5f34c322679d1327016e9e294faf" integrity sha1-G67AG16PXzTDImedEycBbp4pT68= -typescript@3.5.3, typescript@^3.0.3, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.0.3, typescript@~3.3.3333, typescript@~3.4.3: +typescript@3.5.1, typescript@3.5.3, typescript@^3.0.3, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.0.3, typescript@~3.3.3333, typescript@~3.4.3: version "3.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== From 40c8f1bbc21ea7e28ac55431c7ada278daea8b79 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Wed, 24 Jul 2019 08:19:56 -0600 Subject: [PATCH 7/7] Ensure visualizations in percentage mode respect the advanced settings percent format (#39044) (#41855) --- .../public/metric_vis_controller.js | 6 +- .../tooltip/_pointseries_tooltip_formatter.js | 7 +- .../ui/public/vislib/__tests__/lib/y_axis.js | 10 - .../__tests__/visualizations/gauge_chart.js | 9 - .../ui/public/vislib/lib/axis/axis_config.js | 5 - .../vislib/visualizations/gauges/meter.js | 4 +- ...st.js.snap => build_pipeline.test.ts.snap} | 20 +- .../pipeline_helpers/build_pipeline.test.js | 262 --------- .../pipeline_helpers/build_pipeline.test.ts | 510 ++++++++++++++++++ .../loader/pipeline_helpers/build_pipeline.ts | 42 +- .../apps/dashboard/dashboard_filtering.js | 2 +- .../apps/dashboard/embeddable_rendering.js | 2 +- .../functional/apps/visualize/_gauge_chart.js | 27 +- .../apps/visualize/_vertical_bar_chart.js | 13 + .../functional/page_objects/visualize_page.js | 5 + 15 files changed, 606 insertions(+), 318 deletions(-) rename src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/{build_pipeline.test.js.snap => build_pipeline.test.ts.snap} (67%) delete mode 100644 src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js create mode 100644 src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts diff --git a/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js b/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js index e8c646646ec4b..ecbb9d917874c 100644 --- a/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js +++ b/src/legacy/core_plugins/metric_vis/public/metric_vis_controller.js @@ -119,11 +119,9 @@ export class MetricVisComponent extends Component { const color = this._getColor(value, labels, colors); if (isPercentageMode) { - const percentage = Math.round(100 * (value - min) / (max - min)); - value = `${percentage}%`; - } else { - value = this._getFormattedValue(formatter, value, 'html'); + value = (value - min) / (max - min); } + value = this._getFormattedValue(formatter, value, 'html'); if (bucketColumnId) { const bucketValue = this._getFormattedValue(bucketFormatter, row[bucketColumnId]); diff --git a/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip_formatter.js b/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip_formatter.js index 4c0aa00441747..8bf1c763a58c4 100644 --- a/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip_formatter.js +++ b/src/legacy/ui/public/vis/components/tooltip/_pointseries_tooltip_formatter.js @@ -49,12 +49,7 @@ export function PointSeriesTooltipFormatterProvider($compile, $rootScope) { } if (datum.y) { const value = datum.yScale ? datum.yScale * datum.y : datum.y; - if(event.isPercentageMode) { - const valueInPercent = Math.round(value * 10000) / 100; - addDetail(currentSeries.label, `${valueInPercent.toFixed(2)} %`); - } else { - addDetail(currentSeries.label, currentSeries.yAxisFormatter(value)); - } + addDetail(currentSeries.label, currentSeries.yAxisFormatter(value)); } if (datum.z) { addDetail(currentSeries.zLabel, currentSeries.zAxisFormatter(datum.z)); diff --git a/src/legacy/ui/public/vislib/__tests__/lib/y_axis.js b/src/legacy/ui/public/vislib/__tests__/lib/y_axis.js index e8b340229544c..ea3f3c5f26320 100644 --- a/src/legacy/ui/public/vislib/__tests__/lib/y_axis.js +++ b/src/legacy/ui/public/vislib/__tests__/lib/y_axis.js @@ -342,16 +342,6 @@ describe('Vislib yAxis Class Test Suite', function () { yAxis = buildYAxis(); }); - it('should use percentage format for percentages', function () { - yAxis = buildYAxis({ - scale: { - mode: 'percentage' - } - }); - const tickFormat = yAxis.getAxis().tickFormat(); - expect(tickFormat(1)).to.be('100%'); - }); - it('should use decimal format for small values', function () { yAxis.yMax = 1; const tickFormat = yAxis.getAxis().tickFormat(); diff --git a/src/legacy/ui/public/vislib/__tests__/visualizations/gauge_chart.js b/src/legacy/ui/public/vislib/__tests__/visualizations/gauge_chart.js index bb4aa5679b374..13fdb1f054371 100644 --- a/src/legacy/ui/public/vislib/__tests__/visualizations/gauge_chart.js +++ b/src/legacy/ui/public/vislib/__tests__/visualizations/gauge_chart.js @@ -113,15 +113,6 @@ describe('Vislib Gauge Chart Test Suite', function () { expect($(chartEl).find('svg').length).to.equal(5); }); - it('creates gauge with percentage mode', function () { - generateVis({ - gauge: { - percentageMode: true - } - }); - expect($(chartEl).find('svg > g > g > text').text()).to.equal('94%77%61%24%45%'); - }); - it('creates gauge with automatic mode', function () { generateVis({ gauge: { diff --git a/src/legacy/ui/public/vislib/lib/axis/axis_config.js b/src/legacy/ui/public/vislib/lib/axis/axis_config.js index 7c49d7d39d2a9..1e0ec01933b87 100644 --- a/src/legacy/ui/public/vislib/lib/axis/axis_config.js +++ b/src/legacy/ui/public/vislib/lib/axis/axis_config.js @@ -136,11 +136,6 @@ export class AxisConfig { } } - // override axisFormatter (to replicate current behaviour) - if (this.isPercentage()) { - this._values.labels.axisFormatter = d3.format('%'); - } - if (this.isLogScale()) { this._values.labels.filter = true; } diff --git a/src/legacy/ui/public/vislib/visualizations/gauges/meter.js b/src/legacy/ui/public/vislib/visualizations/gauges/meter.js index 834d579b2d93f..aa2a92842d383 100644 --- a/src/legacy/ui/public/vislib/visualizations/gauges/meter.js +++ b/src/legacy/ui/public/vislib/visualizations/gauges/meter.js @@ -264,8 +264,8 @@ export class MeterGauge { .attr('y', -5) .text(d => { if (this.gaugeConfig.percentageMode) { - const percentage = Math.round(100 * (d.y - min) / (max - min)); - return `${percentage}%`; + const percentage = (d.y - min) / (max - min); + return data.yAxisFormatter(percentage); } return data.yAxisFormatter(d.y); }) diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap b/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.ts.snap similarity index 67% rename from src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap rename to src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.ts.snap index a6300be4f84a2..3da7a91378827 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.js.snap +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/__snapshots__/build_pipeline.test.ts.snap @@ -8,25 +8,27 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function with buckets 1`] = `"metricvis metric={visdimension 0 } metric={visdimension 1 } "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function with percentage mode should have percentage format 1`] = `"metricvis percentage=true metric={visdimension 0 format='percent' } "`; + exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function without buckets 1`] = `"metricvis metric={visdimension 0 } metric={visdimension 1 } "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metrics/tsvb function 1`] = `"tsvb params='{\\"foo\\":\\"bar\\"}' uiState='{}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles pie function 1`] = `"kibana_pie visConfig='{\\"dimensions\\":{\\"metric\\":0,\\"buckets\\":[1,2]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles pie function 1`] = `"kibana_pie visConfig='{\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"buckets\\":[1,2]}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":0,\\"bucket\\":1}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"bucket\\":1}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":0}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=false 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":false,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[4,5],\\"buckets\\":[0,3]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=false 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":false,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":4,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=true 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[1,2,4,5],\\"buckets\\":[0,3]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=true 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":1,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":2,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":4,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":5,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[0,3]}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[0],\\"buckets\\":[],\\"splitRow\\":[1,2]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[],\\"splitRow\\":[1,2]}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits and buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[0,1],\\"buckets\\":[3],\\"splitRow\\":[2,4]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits and buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[3],\\"splitRow\\":[2,4]}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function without splits or buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[0,1],\\"buckets\\":[]}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function without splits or buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[{\\"accessor\\":0,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},{\\"accessor\\":1,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}],\\"buckets\\":[]}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function with boolean param showLabel 1`] = `"tagcloud metric={visdimension 0} showLabel=false "`; @@ -34,7 +36,7 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function without buckets 1`] = `"tagcloud metric={visdimension 0} "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":0,\\"geohash\\":1,\\"geocentroid\\":3}}' "`; +exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles timelion function 1`] = `"timelion_vis expression='foo' interval='bar' "`; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js deleted file mode 100644 index 2a3d4d0ddab07..0000000000000 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.js +++ /dev/null @@ -1,262 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { prepareJson, prepareString, buildPipelineVisFunction, buildPipeline } from './build_pipeline'; - -jest.mock('ui/agg_types/buckets/date_histogram', () => ({})); - -describe('visualize loader pipeline helpers: build pipeline', () => { - describe('prepareJson', () => { - it('returns a correctly formatted key/value string', () => { - const expected = `foo='{}' `; // trailing space is expected - const actual = prepareJson('foo', {}); - expect(actual).toBe(expected); - }); - - it('stringifies provided data', () => { - const expected = `foo='{\"well\":\"hello\",\"there\":{\"friend\":true}}' `; - const actual = prepareJson('foo', { well: 'hello', there: { friend: true } }); - expect(actual).toBe(expected); - }); - - it('escapes single quotes', () => { - const expected = `foo='{\"well\":\"hello \\'hi\\'\",\"there\":{\"friend\":true}}' `; - const actual = prepareJson('foo', { well: `hello 'hi'`, there: { friend: true } }); - expect(actual).toBe(expected); - }); - }); - - describe('prepareString', () => { - it('returns a correctly formatted key/value string', () => { - const expected = `foo='bar' `; // trailing space is expected - const actual = prepareString('foo', 'bar'); - expect(actual).toBe(expected); - }); - - it('escapes single quotes', () => { - const expected = `foo='\\'bar\\'' `; - const actual = prepareString('foo', `'bar'`); - expect(actual).toBe(expected); - }); - }); - - describe('buildPipelineVisFunction', () => { - it('handles vega function', () => { - const params = { spec: 'this is a test' }; - const actual = buildPipelineVisFunction.vega({ params }); - expect(actual).toMatchSnapshot(); - }); - - it('handles input_control_vis function', () => { - const params = { - some: 'nested', - data: { - here: true - } - }; - const actual = buildPipelineVisFunction.input_control_vis({ params }); - expect(actual).toMatchSnapshot(); - }); - - it('handles metrics/tsvb function', () => { - const params = { foo: 'bar' }; - const actual = buildPipelineVisFunction.metrics({ params }); - expect(actual).toMatchSnapshot(); - }); - - it('handles timelion function', () => { - const params = { expression: 'foo', interval: 'bar' }; - const actual = buildPipelineVisFunction.timelion({ params }); - expect(actual).toMatchSnapshot(); - }); - - it('handles markdown function', () => { - const params = { markdown: '## hello _markdown_', fontSize: 12, openLinksInNewTab: true, foo: 'bar' }; - const actual = buildPipelineVisFunction.markdown({ params }); - expect(actual).toMatchSnapshot(); - }); - - it('handles undefined markdown function', () => { - const params = { fontSize: 12, openLinksInNewTab: true, foo: 'bar' }; - const actual = buildPipelineVisFunction.markdown({ params }); - expect(actual).toMatchSnapshot(); - }); - - describe('handles table function', () => { - it('without splits or buckets', () => { - const params = { foo: 'bar' }; - const schemas = { metric: [0, 1] }; - const actual = buildPipelineVisFunction.table({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - it('with splits', () => { - const params = { foo: 'bar' }; - const schemas = { - metric: [0], - split_row: [1, 2], - }; - const actual = buildPipelineVisFunction.table({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - it('with splits and buckets', () => { - const params = { foo: 'bar' }; - const schemas = { - metric: [0, 1], - split_row: [2, 4], - bucket: [3], - }; - const actual = buildPipelineVisFunction.table({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=true', () => { - const params = { - showMetricsAtAllLevels: true, - showPartialRows: true, - }; - const schemas = { - metric: [1, 2, 4, 5], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - it('with showPartialRows=true and showMetricsAtAllLevels=false', () => { - const params = { - showMetricsAtAllLevels: false, - showPartialRows: true, - }; - const schemas = { - metric: [1, 2, 4, 5], - bucket: [0, 3], - }; - const actual = buildPipelineVisFunction.table({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - }); - - describe('handles metric function', () => { - const params = { metric: {} }; - it('without buckets', () => { - const schemas = { metric: [{ accessor: 0 }, { accessor: 1 }] }; - const actual = buildPipelineVisFunction.metric({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - it('with buckets', () => { - const schemas = { - metric: [{ accessor: 0 }, { accessor: 1 }], - group: [{ accessor: 2 }] - }; - const actual = buildPipelineVisFunction.metric({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - }); - - describe('handles tagcloud function', () => { - const params = {}; - - it('without buckets', () => { - const schemas = { metric: [{ accessor: 0 }] }; - const actual = buildPipelineVisFunction.tagcloud({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - it('with buckets', () => { - const schemas = { - metric: [{ accessor: 0 }], - segment: [{ accessor: 1 }] - }; - const actual = buildPipelineVisFunction.tagcloud({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - it('with boolean param showLabel', () => { - const schemas = { metric: [{ accessor: 0 }] }; - const params = { showLabel: false }; - const actual = buildPipelineVisFunction.tagcloud({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - }); - - describe('handles region_map function', () => { - const params = { metric: {} }; - it('without buckets', () => { - const schemas = { metric: [0] }; - const actual = buildPipelineVisFunction.region_map({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - it('with buckets', () => { - const schemas = { - metric: [0], - segment: [1, 2] - }; - const actual = buildPipelineVisFunction.region_map({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - }); - - it('handles tile_map function', () => { - const params = { metric: {} }; - const schemas = { - metric: [0], - segment: [1, 2], - geo_centroid: [3, 4] - }; - const actual = buildPipelineVisFunction.tile_map({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - - it('handles pie function', () => { - const params = {}; - const schemas = { - metric: [0], - segment: [1, 2] - }; - const actual = buildPipelineVisFunction.pie({ params }, schemas); - expect(actual).toMatchSnapshot(); - }); - }); - - describe('buildPipeline', () => { - it('calls toExpression on vis_type if it exists', async () => { - const vis = { - getCurrentState: () => {}, - getUiState: () => null, - isHierarchical: () => false, - aggs: { - getResponseAggs: () => [], - }, - type: { - toExpression: () => 'testing custom expressions', - } - }; - const searchSource = { - getField: () => null, - }; - const expression = await buildPipeline(vis, { searchSource }); - expect(expression).toMatchSnapshot(); - }); - }); -}); diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts new file mode 100644 index 0000000000000..d98872cb3402f --- /dev/null +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.test.ts @@ -0,0 +1,510 @@ +/* + * 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 { + prepareJson, + prepareString, + buildPipelineVisFunction, + buildVislibDimensions, + buildPipeline, + SchemaConfig, + Schemas, +} from './build_pipeline'; +import { Vis, VisState } from 'ui/vis'; +import { AggConfig } from 'ui/vis/agg_config'; +import { SearchSource } from 'ui/courier'; + +jest.mock('ui/agg_types/buckets/date_histogram', () => ({})); + +describe('visualize loader pipeline helpers: build pipeline', () => { + describe('prepareJson', () => { + it('returns a correctly formatted key/value string', () => { + const expected = `foo='{}' `; // trailing space is expected + const actual = prepareJson('foo', {}); + expect(actual).toBe(expected); + }); + + it('stringifies provided data', () => { + const expected = `foo='{\"well\":\"hello\",\"there\":{\"friend\":true}}' `; + const actual = prepareJson('foo', { well: 'hello', there: { friend: true } }); + expect(actual).toBe(expected); + }); + + it('escapes single quotes', () => { + const expected = `foo='{\"well\":\"hello \\'hi\\'\",\"there\":{\"friend\":true}}' `; + const actual = prepareJson('foo', { well: `hello 'hi'`, there: { friend: true } }); + expect(actual).toBe(expected); + }); + }); + + describe('prepareString', () => { + it('returns a correctly formatted key/value string', () => { + const expected = `foo='bar' `; // trailing space is expected + const actual = prepareString('foo', 'bar'); + expect(actual).toBe(expected); + }); + + it('escapes single quotes', () => { + const expected = `foo='\\'bar\\'' `; + const actual = prepareString('foo', `'bar'`); + expect(actual).toBe(expected); + }); + }); + + describe('buildPipelineVisFunction', () => { + let visStateDef: VisState; + let schemaConfig: SchemaConfig; + let schemasDef: Schemas; + let uiState: any; + + beforeEach(() => { + visStateDef = { + title: 'title', + // @ts-ignore + type: 'type', + params: {}, + }; + + schemaConfig = { + accessor: 0, + format: {}, + params: {}, + aggType: '', + }; + + schemasDef = { metric: [schemaConfig] }; + uiState = {}; + }); + + it('handles vega function', () => { + const vis = { + ...visStateDef, + params: { spec: 'this is a test' }, + }; + const actual = buildPipelineVisFunction.vega(vis, schemasDef, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('handles input_control_vis function', () => { + const visState = { + ...visStateDef, + params: { + some: 'nested', + data: { here: true }, + }, + }; + const actual = buildPipelineVisFunction.input_control_vis(visState, schemasDef, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('handles metrics/tsvb function', () => { + const visState = { ...visStateDef, params: { foo: 'bar' } }; + const actual = buildPipelineVisFunction.metrics(visState, schemasDef, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('handles timelion function', () => { + const visState = { + ...visStateDef, + params: { expression: 'foo', interval: 'bar' }, + }; + const actual = buildPipelineVisFunction.timelion(visState, schemasDef, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('handles markdown function', () => { + const visState = { + ...visStateDef, + params: { + markdown: '## hello _markdown_', + fontSize: 12, + openLinksInNewTab: true, + foo: 'bar', + }, + }; + const actual = buildPipelineVisFunction.markdown(visState, schemasDef, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('handles undefined markdown function', () => { + const visState = { + ...visStateDef, + params: { fontSize: 12, openLinksInNewTab: true, foo: 'bar' }, + }; + const actual = buildPipelineVisFunction.markdown(visState, schemasDef, uiState); + expect(actual).toMatchSnapshot(); + }); + + describe('handles table function', () => { + it('without splits or buckets', () => { + const visState = { ...visStateDef, params: { foo: 'bar' } }; + const schemas = { + ...schemasDef, + metric: [{ ...schemaConfig, accessor: 0 }, { ...schemaConfig, accessor: 1 }], + }; + const actual = buildPipelineVisFunction.table(visState, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('with splits', () => { + const visState = { ...visStateDef, params: { foo: 'bar' } }; + const schemas = { + ...schemasDef, + split_row: [1, 2], + }; + const actual = buildPipelineVisFunction.table(visState, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('with splits and buckets', () => { + const visState = { ...visStateDef, params: { foo: 'bar' } }; + const schemas = { + ...schemasDef, + metric: [{ ...schemaConfig, accessor: 0 }, { ...schemaConfig, accessor: 1 }], + split_row: [2, 4], + bucket: [3], + }; + const actual = buildPipelineVisFunction.table(visState, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('with showPartialRows=true and showMetricsAtAllLevels=true', () => { + const visState = { + ...visStateDef, + params: { + showMetricsAtAllLevels: true, + showPartialRows: true, + }, + }; + const schemas = { + ...schemasDef, + metric: [ + { ...schemaConfig, accessor: 1 }, + { ...schemaConfig, accessor: 2 }, + { ...schemaConfig, accessor: 4 }, + { ...schemaConfig, accessor: 5 }, + ], + bucket: [0, 3], + }; + const actual = buildPipelineVisFunction.table(visState, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('with showPartialRows=true and showMetricsAtAllLevels=false', () => { + const visState = { + ...visStateDef, + params: { + showMetricsAtAllLevels: false, + showPartialRows: true, + }, + }; + const schemas = { + ...schemasDef, + metric: [ + { ...schemaConfig, accessor: 1 }, + { ...schemaConfig, accessor: 2 }, + { ...schemaConfig, accessor: 4 }, + { ...schemaConfig, accessor: 5 }, + ], + bucket: [0, 3], + }; + const actual = buildPipelineVisFunction.table(visState, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + }); + + describe('handles metric function', () => { + it('without buckets', () => { + const visState = { ...visStateDef, params: { metric: {} } }; + const schemas = { + ...schemasDef, + metric: [{ ...schemaConfig, accessor: 0 }, { ...schemaConfig, accessor: 1 }], + }; + const actual = buildPipelineVisFunction.metric(visState, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('with buckets', () => { + const visState = { ...visStateDef, params: { metric: {} } }; + const schemas = { + ...schemasDef, + metric: [{ ...schemaConfig, accessor: 0 }, { ...schemaConfig, accessor: 1 }], + group: [{ accessor: 2 }], + }; + const actual = buildPipelineVisFunction.metric(visState, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('with percentage mode should have percentage format', () => { + const visState = { ...visStateDef, params: { metric: { percentageMode: true } } }; + const schemas = { ...schemasDef }; + const actual = buildPipelineVisFunction.metric(visState, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + }); + + describe('handles tagcloud function', () => { + it('without buckets', () => { + const actual = buildPipelineVisFunction.tagcloud(visStateDef, schemasDef, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('with buckets', () => { + const schemas = { + ...schemasDef, + segment: [{ accessor: 1 }], + }; + const actual = buildPipelineVisFunction.tagcloud(visStateDef, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('with boolean param showLabel', () => { + const visState = { ...visStateDef, params: { showLabel: false } }; + const actual = buildPipelineVisFunction.tagcloud(visState, schemasDef, uiState); + expect(actual).toMatchSnapshot(); + }); + }); + + describe('handles region_map function', () => { + it('without buckets', () => { + const visState = { ...visStateDef, params: { metric: {} } }; + const actual = buildPipelineVisFunction.region_map(visState, schemasDef, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('with buckets', () => { + const schemas = { + ...schemasDef, + segment: [1, 2], + }; + const actual = buildPipelineVisFunction.region_map(visStateDef, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + }); + + it('handles tile_map function', () => { + const visState = { ...visStateDef, params: { metric: {} } }; + const schemas = { + ...schemasDef, + segment: [1, 2], + geo_centroid: [3, 4], + }; + const actual = buildPipelineVisFunction.tile_map(visState, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + + it('handles pie function', () => { + const schemas = { + ...schemasDef, + segment: [1, 2], + }; + const actual = buildPipelineVisFunction.pie(visStateDef, schemas, uiState); + expect(actual).toMatchSnapshot(); + }); + }); + + describe('buildPipeline', () => { + it('calls toExpression on vis_type if it exists', async () => { + const vis: Vis = { + getCurrentState: () => {}, + getUiState: () => null, + isHierarchical: () => false, + aggs: { + getResponseAggs: () => [], + }, + // @ts-ignore + type: { + toExpression: () => 'testing custom expressions', + }, + }; + const searchSource: SearchSource = { + getField: () => null, + }; + const expression = await buildPipeline(vis, { searchSource }); + expect(expression).toMatchSnapshot(); + }); + }); + + describe('buildVislibDimensions', () => { + let aggs: AggConfig[]; + let visState: any; + let vis: Vis; + let params: any; + + beforeEach(() => { + aggs = [ + { + id: 0, + enabled: true, + type: { + type: 'metrics', + name: 'count', + }, + schema: { + name: 'metric', + }, + params: {}, + }, + ]; + + params = { + searchSource: null, + timeRange: null, + }; + }); + + // todo: cover basic buildVislibDimensions's functionalities + + describe('test y dimension format for histogram chart', () => { + beforeEach(() => { + visState = { + params: { + seriesParams: [ + { + data: { id: 0 }, + valueAxis: 'axis-y', + }, + ], + valueAxes: [ + { + id: 'axis-y', + scale: { + mode: 'normal', + }, + }, + ], + }, + }; + + vis = { + // @ts-ignore + type: { + name: 'histogram', + }, + aggs: { + getResponseAggs: () => { + return aggs; + }, + }, + isHierarchical: () => { + return false; + }, + getCurrentState: () => { + return visState; + }, + }; + }); + + it('with one numeric metric in regular moder', async () => { + const dimensions = await buildVislibDimensions(vis, params); + const expected = { id: 'number' }; + const actual = dimensions.y[0].format; + expect(actual).toEqual(expected); + }); + + it('with one numeric metric in percentage mode', async () => { + visState.params.valueAxes[0].scale.mode = 'percentage'; + const dimensions = await buildVislibDimensions(vis, params); + const expected = { id: 'percent' }; + const actual = dimensions.y[0].format; + expect(actual).toEqual(expected); + }); + + it('with two numeric metrics, mixed normal and percent mode should have corresponding formatters', async () => { + const aggConfig = aggs[0]; + aggs = [{ ...aggConfig }, { ...aggConfig, id: 5 }]; + + visState = { + params: { + seriesParams: [ + { + data: { id: 0 }, + valueAxis: 'axis-y-1', + }, + { + data: { id: 5 }, + valueAxis: 'axis-y-2', + }, + ], + valueAxes: [ + { + id: 'axis-y-1', + scale: { + mode: 'normal', + }, + }, + { + id: 'axis-y-2', + scale: { + mode: 'percentage', + }, + }, + ], + }, + }; + + const dimensions = await buildVislibDimensions(vis, params); + const expectedY1 = { id: 'number' }; + const expectedY2 = { id: 'percent' }; + expect(dimensions.y[0].format).toEqual(expectedY1); + expect(dimensions.y[1].format).toEqual(expectedY2); + }); + }); + + describe('test y dimension format for gauge chart', () => { + beforeEach(() => { + visState = { params: { gauge: {} } }; + + vis = { + // @ts-ignore + type: { + name: 'gauge', + }, + aggs: { + getResponseAggs: () => { + return aggs; + }, + }, + isHierarchical: () => { + return false; + }, + getCurrentState: () => { + return visState; + }, + }; + }); + + it('with percentageMode = false', async () => { + visState.params.gauge.percentageMode = false; + const dimensions = await buildVislibDimensions(vis, params); + const expected = { id: 'number' }; + const actual = dimensions.y[0].format; + expect(actual).toEqual(expected); + }); + + it('with percentageMode = true', async () => { + visState.params.gauge.percentageMode = true; + const dimensions = await buildVislibDimensions(vis, params); + const expected = { id: 'percent' }; + const actual = dimensions.y[0].format; + expect(actual).toEqual(expected); + }); + }); + }); +}); diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts index 7ae6308668f1a..38a53b7078d4c 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/build_pipeline.ts @@ -17,7 +17,7 @@ * under the License. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, get } from 'lodash'; // @ts-ignore import { setBounds } from 'ui/agg_types/buckets/date_histogram'; import { SearchSource } from 'ui/courier'; @@ -34,14 +34,14 @@ interface SchemaConfigParams { useGeocentroid?: boolean; } -interface SchemaConfig { +export interface SchemaConfig { accessor: number; format: SchemaFormat | {}; params: SchemaConfigParams; aggType: string; } -interface Schemas { +export interface Schemas { metric: SchemaConfig[]; bucket?: any[]; geo_centroid?: any[]; @@ -153,10 +153,6 @@ export const getSchemas = (vis: Vis, timeRange?: any): Schemas => { const isHierarchical = vis.isHierarchical(); const metrics = responseAggs.filter((agg: AggConfig) => agg.type.type === 'metrics'); responseAggs.forEach((agg: AggConfig) => { - if (!agg.enabled) { - cnt++; - return; - } let skipMetrics = false; let schemaName = agg.schema ? agg.schema.name || agg.schema : null; if (typeof schemaName === 'object') { @@ -244,6 +240,30 @@ export const prepareDimension = (variable: string, data: any) => { return expr; }; +const adjustVislibDimensionFormmaters = (vis: Vis, dimensions: { y: any[] }): void => { + const visState = vis.getCurrentState(); + const visConfig = visState.params; + const responseAggs = vis.aggs.getResponseAggs().filter((agg: AggConfig) => agg.enabled); + + (dimensions.y || []).forEach(yDimension => { + const yAgg = responseAggs[yDimension.accessor]; + const seriesParam = (visConfig.seriesParams || []).find( + (param: any) => param.data.id === yAgg.id + ); + if (seriesParam) { + const usedValueAxis = (visConfig.valueAxes || []).find( + (valueAxis: any) => valueAxis.id === seriesParam.valueAxis + ); + if (get(usedValueAxis, 'scale.mode') === 'percentage') { + yDimension.format = { id: 'percent' }; + } + } + if (get(visConfig, 'gauge.percentageMode') === true) { + yDimension.format = { id: 'percent' }; + } + }); +}; + export const buildPipelineVisFunction: BuildPipelineVisFunction = { vega: visState => { return `vega ${prepareString('spec', visState.params.spec)}`; @@ -293,6 +313,13 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { } = visState.params.metric; const { metrics, bucket } = buildVisConfig.metric(schemas).dimensions; + // fix formatter for percentage mode + if (get(visState.params, 'metric.percentageMode') === true) { + metrics.forEach((metric: SchemaConfig) => { + metric.format = { id: 'percent' }; + }); + } + let expr = `metricvis `; expr += prepareValue('percentage', percentageMode); expr += prepareValue('colorScheme', colorSchema); @@ -452,6 +479,7 @@ export const buildVislibDimensions = async ( } } + adjustVislibDimensionFormmaters(vis, dimensions); return dimensions; }; diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index fb32de7832b4d..5eddd3d8abb31 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -189,7 +189,7 @@ export default function ({ getService, getPageObjects }) { }); it('goal and guages', async () => { - await dashboardExpect.goalAndGuageLabelsExist(['40%', '7,544']); + await dashboardExpect.goalAndGuageLabelsExist(['39.958%', '7,544']); }); it('tsvb time series', async () => { diff --git a/test/functional/apps/dashboard/embeddable_rendering.js b/test/functional/apps/dashboard/embeddable_rendering.js index 031348fa005e7..831622716f381 100644 --- a/test/functional/apps/dashboard/embeddable_rendering.js +++ b/test/functional/apps/dashboard/embeddable_rendering.js @@ -45,7 +45,7 @@ export default function ({ getService, getPageObjects }) { await dashboardExpect.timelionLegendCount(0); await dashboardExpect.markdownWithValuesExists(['I\'m a markdown!']); await dashboardExpect.vegaTextsExist(['5,000']); - await dashboardExpect.goalAndGuageLabelsExist(['63%', '56%', '11.915 GB']); + await dashboardExpect.goalAndGuageLabelsExist(['62.925%', '55.625%', '11.915 GB']); await dashboardExpect.dataTableRowCount(5); await dashboardExpect.tagCloudWithValuesFound(['CN', 'IN', 'US', 'BR', 'ID']); // TODO add test for 'region map viz' diff --git a/test/functional/apps/visualize/_gauge_chart.js b/test/functional/apps/visualize/_gauge_chart.js index 07f7cfd3fe159..6b14cabb54f99 100644 --- a/test/functional/apps/visualize/_gauge_chart.js +++ b/test/functional/apps/visualize/_gauge_chart.js @@ -23,6 +23,7 @@ export default function ({ getService, getPageObjects }) { const log = getService('log'); const retry = getService('retry'); const inspector = getService('inspector'); + const find = getService('find'); const PageObjects = getPageObjects(['common', 'visualize', 'timePicker']); describe('gauge chart', function indexPatternCreation() { @@ -30,15 +31,16 @@ export default function ({ getService, getPageObjects }) { const fromTime = '2015-09-19 06:31:44.000'; const toTime = '2015-09-23 18:31:44.000'; - before(async function () { + async function initGaugeVis() { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); log.debug('clickGauge'); await PageObjects.visualize.clickGauge(); await PageObjects.visualize.clickNewSearch(); await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - }); + } + before(initGaugeVis); it('should have inspector enabled', async function () { await inspector.expectIsEnabled(); @@ -94,5 +96,26 @@ export default function ({ getService, getPageObjects }) { }); }); + it('should format the metric correctly in percentage mode', async function () { + await initGaugeVis(); + await PageObjects.visualize.clickMetricEditor(); + await PageObjects.visualize.selectAggregation('Average', 'metrics'); + await PageObjects.visualize.selectField('bytes', 'metrics'); + await PageObjects.visualize.clickOptionsTab(); + const table = await find.byClassName('visEditorAgg__rangesTable'); + const lastRow = await table.findByCssSelector('tr:last-child'); + const toCell = await lastRow.findByCssSelector('td:nth-child(2) input'); + await toCell.clearValue(); + await toCell.type('10000', { charByChar: true }); + await find.clickByCssSelector('#percentageMode'); + await PageObjects.visualize.waitForVisualizationRenderingStabilized(); + await PageObjects.visualize.clickGo(); + + await retry.try(async function tryingForTime() { + const expectedTexts = [ '57.273%', 'Average bytes' ]; + const metricValue = await PageObjects.visualize.getGaugeValue(); + expect(expectedTexts).to.eql(metricValue); + }); + }); }); } diff --git a/test/functional/apps/visualize/_vertical_bar_chart.js b/test/functional/apps/visualize/_vertical_bar_chart.js index 0a0e64d8e8919..a2ff9d0419fc6 100644 --- a/test/functional/apps/visualize/_vertical_bar_chart.js +++ b/test/functional/apps/visualize/_vertical_bar_chart.js @@ -227,6 +227,19 @@ export default function ({ getService, getPageObjects }) { }); }); + describe('vertical bar in percent mode', async () => { + it('should show ticks with percentage values', async function () { + const axisId = 'ValueAxis-1'; + await PageObjects.visualize.clickMetricsAndAxes(); + await PageObjects.visualize.clickYAxisOptions(axisId); + await PageObjects.visualize.selectYAxisMode('percentage'); + await PageObjects.visualize.clickGo(); + const labels = await PageObjects.visualize.getYAxisLabels(); + expect(labels[0]).to.eql('0%'); + expect(labels[labels.length - 1]).to.eql('100%'); + }); + }); + describe('vertical bar with Split series', function () { before(initBarChart); diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index fe93c5999c90a..c84debcfecbde 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -707,6 +707,11 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli await selector.click(); } + async selectYAxisMode(mode) { + const selector = await find.byCssSelector(`#valueAxisMode0 > option[label="${mode}"]`); + await selector.click(); + } + async clickData() { await testSubjects.click('visualizeEditDataLink'); }