diff --git a/.backportrc.json b/.backportrc.json index 0894909d2aac4..87bc3a1be583b 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -25,7 +25,7 @@ ], "targetPRLabels": ["backport"], "branchLabelMapping": { - "^v7.8.0$": "7.x", + "^v7.9.0$": "7.x", "^v(\\d+).(\\d+).\\d+$": "$1.$2" } } diff --git a/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc index 36709c2cc6437..9a1e81670b654 100644 --- a/docs/visualize/tsvb.asciidoc +++ b/docs/visualize/tsvb.asciidoc @@ -122,3 +122,17 @@ Edit the source for the Markdown visualization. . To insert the mustache template variable into the editor, click the variable name. + The http://mustache.github.io/mustache.5.html[mustache syntax] uses the Handlebar.js processor, which is an extended version of the Mustache template language. + +[float] +[[tsvb-style-markdown]] +==== Style Markdown text + +Style your Markdown visualization using http://lesscss.org/features/[less syntax]. + +. Select *Markdown*. + +. Select *Panel options*. + +. Enter styling rules in *Custom CSS* section ++ +Less in TSVB does not support custom plugins or inline JavaScript. diff --git a/package.json b/package.json index 8a92b46489308..0c83cb429b651 100644 --- a/package.json +++ b/package.json @@ -210,7 +210,7 @@ "leaflet-responsive-popup": "0.6.4", "leaflet-vega": "^0.8.6", "leaflet.heat": "0.2.0", - "less": "^2.7.3", + "less": "npm:@elastic/less@2.7.3-kibana", "less-loader": "5.0.0", "lodash": "npm:@elastic/lodash@3.10.1-kibana4", "lodash.clonedeep": "^4.5.0", diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts index bdb1436c37efb..83335a6fabfeb 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts @@ -96,18 +96,21 @@ export function getTabs( tabs.push({ name: getTitle('indexed', filteredCount, totalCount), id: TAB_INDEXED_FIELDS, + 'data-test-subj': 'tab-indexedFields', }); if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { tabs.push({ name: getTitle('scripted', filteredCount, totalCount), id: TAB_SCRIPTED_FIELDS, + 'data-test-subj': 'tab-scriptedFields', }); } tabs.push({ name: getTitle('sourceFilters', filteredCount, totalCount), id: TAB_SOURCE_FILTERS, + 'data-test-subj': 'tab-sourceFilters', }); return tabs; diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index 1629aac588a61..21e7b559f71f5 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -22,6 +22,7 @@ jest.mock('history'); import { setRootControllerMock, historyMock } from './new_platform.test.mocks'; import { legacyAppRegister, __reset__, __setup__, __start__ } from './new_platform'; import { coreMock } from '../../../../core/public/mocks'; +import { AppMount } from '../../../../core/public'; describe('ui/new_platform', () => { describe('legacyAppRegister', () => { @@ -33,7 +34,7 @@ describe('ui/new_platform', () => { const registerApp = () => { const unmountMock = jest.fn(); - const mountMock = jest.fn(() => unmountMock); + const mountMock = jest.fn, Parameters>(() => unmountMock); legacyAppRegister({ id: 'test', title: 'Test', @@ -62,13 +63,25 @@ describe('ui/new_platform', () => { controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith({ - element: elementMock[0], + element: expect.any(HTMLElement), appBasePath: '/test/base/path/app/test', onAppLeave: expect.any(Function), history: historyMock, }); }); + test('app is mounted in new div inside containing element', () => { + const { mountMock } = registerApp(); + const controller = setRootControllerMock.mock.calls[0][1]; + const scopeMock = { $on: jest.fn() }; + const elementMock = [document.createElement('div')]; + + controller(scopeMock, elementMock); + + const { element } = mountMock.mock.calls[0][0]; + expect(element.parentElement).toEqual(elementMock[0]); + }); + test('controller calls deprecated context app.mount when invoked', () => { const unmountMock = jest.fn(); // Two arguments changes how this is called. @@ -84,7 +97,7 @@ describe('ui/new_platform', () => { controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { - element: elementMock[0], + element: expect.any(HTMLElement), appBasePath: '/test/base/path/app/test', onAppLeave: expect.any(Function), history: historyMock, diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index a15c7cce5511d..1eb46e1a43895 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -176,7 +176,8 @@ export const legacyAppRegister = (app: App) => { legacyAppRegistered = true; require('ui/chrome').setRootController(app.id, ($scope: IScope, $element: JQLite) => { - const element = $element[0]; + const element = document.createElement('div'); + $element[0].appendChild(element); // Root controller cannot return a Promise so use an internal async function and call it immediately (async () => { diff --git a/src/plugins/discover/public/application/_discover.scss b/src/plugins/discover/public/application/_discover.scss index 8eaa66cf58624..590dbebdf4cfe 100644 --- a/src/plugins/discover/public/application/_discover.scss +++ b/src/plugins/discover/public/application/_discover.scss @@ -38,17 +38,7 @@ discover-app { } .dscResultCount { - text-align: center; padding-top: $euiSizeXS; - padding-left: $euiSizeM; - - .dscResultHits { - padding-left: $euiSizeXS; - } - - > .kuiLink { - padding-left: $euiSizeM; - } } .dscTimechart__header { diff --git a/src/plugins/discover/public/application/angular/discover.html b/src/plugins/discover/public/application/angular/discover.html index b4db89b9275b4..a0f98ea38ef78 100644 --- a/src/plugins/discover/public/application/angular/discover.html +++ b/src/plugins/discover/public/application/angular/discover.html @@ -89,24 +89,12 @@

{{screenTitle}}

-
- {{(hits || 0) | number:0}} - - -
+ +
; + + beforeAll(() => { + props = { + onResetQuery: jest.fn(), + showResetButton: true, + hits: 2, + }; + }); + + it('HitsCounter renders a button by providing the showResetButton property', () => { + component = mountWithIntl(); + expect(findTestSubject(component, 'resetSavedSearch').length).toBe(1); + }); + + it('HitsCounter not renders a button when the showResetButton property is false', () => { + component = mountWithIntl( + + ); + expect(findTestSubject(component, 'resetSavedSearch').length).toBe(0); + }); + + it('expect to render the number of hits', function() { + component = mountWithIntl(); + const hits = findTestSubject(component, 'discoverQueryHits'); + expect(hits.text()).toBe('2'); + }); + + it('expect to render 1,899 hits if 1899 hits given', function() { + component = mountWithIntl( + + ); + const hits = findTestSubject(component, 'discoverQueryHits'); + expect(hits.text()).toBe('1,899'); + }); + + it('should reset query', function() { + component = mountWithIntl(); + findTestSubject(component, 'resetSavedSearch').simulate('click'); + expect(props.onResetQuery).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx b/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx new file mode 100644 index 0000000000000..1d2cd12877b1c --- /dev/null +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter.tsx @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { formatNumWithCommas } from '../../helpers'; + +export interface HitsCounterProps { + /** + * the number of query hits + */ + hits: number; + /** + * displays the reset button + */ + showResetButton: boolean; + /** + * resets the query + */ + onResetQuery: () => void; +} + +export function HitsCounter({ hits, showResetButton, onResetQuery }: HitsCounterProps) { + return ( + + + + + {formatNumWithCommas(hits)}{' '} + + + + {showResetButton && ( + + + + + + )} + + + ); +} diff --git a/src/plugins/discover/public/application/components/hits_counter/hits_counter_directive.ts b/src/plugins/discover/public/application/components/hits_counter/hits_counter_directive.ts new file mode 100644 index 0000000000000..8d45e28370cad --- /dev/null +++ b/src/plugins/discover/public/application/components/hits_counter/hits_counter_directive.ts @@ -0,0 +1,27 @@ +/* + * 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 { HitsCounter } from './hits_counter'; + +export function createHitsCounterDirective(reactDirective: any) { + return reactDirective(HitsCounter, [ + ['hits', { watchDepth: 'reference' }], + ['showResetButton', { watchDepth: 'reference' }], + ['onResetQuery', { watchDepth: 'reference' }], + ]); +} diff --git a/src/plugins/discover/public/application/components/hits_counter/index.ts b/src/plugins/discover/public/application/components/hits_counter/index.ts new file mode 100644 index 0000000000000..58e7a9eda7f51 --- /dev/null +++ b/src/plugins/discover/public/application/components/hits_counter/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { HitsCounter } from './hits_counter'; +export { createHitsCounterDirective } from './hits_counter_directive'; diff --git a/src/plugins/discover/public/application/helpers/format_number_with_commas.ts b/src/plugins/discover/public/application/helpers/format_number_with_commas.ts new file mode 100644 index 0000000000000..01a010d823d5f --- /dev/null +++ b/src/plugins/discover/public/application/helpers/format_number_with_commas.ts @@ -0,0 +1,27 @@ +/* + * 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 COMMA_SEPARATOR_RE = /(\d)(?=(\d{3})+(?!\d))/g; + +/** + * Converts a number to a string and adds commas + * as thousands separators + */ +export const formatNumWithCommas = (input: number) => + String(input).replace(COMMA_SEPARATOR_RE, '$1,'); diff --git a/src/plugins/discover/public/application/helpers/index.ts b/src/plugins/discover/public/application/helpers/index.ts index 7196c96989e97..3555d24924e80 100644 --- a/src/plugins/discover/public/application/helpers/index.ts +++ b/src/plugins/discover/public/application/helpers/index.ts @@ -18,3 +18,4 @@ */ export { shortenDottedString } from './shorten_dotted_string'; +export { formatNumWithCommas } from './format_number_with_commas'; diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index e7813c43383f9..8c3f4f030688c 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -57,6 +57,7 @@ import { createTopNavHelper, } from '../../kibana_legacy/public'; import { createDiscoverSidebarDirective } from './application/components/sidebar'; +import { createHitsCounterDirective } from '././application/components/hits_counter'; import { DiscoverStartPlugins } from './plugin'; /** @@ -151,6 +152,7 @@ export function initializeInnerAngularModule( .directive('fixedScroll', FixedScrollProvider) .directive('renderComplete', createRenderCompleteDirective) .directive('discoverSidebar', createDiscoverSidebarDirective) + .directive('hitsCounter', createHitsCounterDirective) .service('debounce', ['$timeout', DebounceProviderTimeout]); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js index ed17fceeda540..4aa8856836fc6 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/markdown.js @@ -64,7 +64,7 @@ class MarkdownPanelConfigUi extends Component { const lessSrc = `#markdown-${model.id} { ${value} }`; - lessC.render(lessSrc, { compress: true }, (e, output) => { + lessC.render(lessSrc, { compress: true, javascriptEnabled: false }, (e, output) => { const parts = { markdown_less: value }; if (output) { parts.markdown_css = output.css; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js index 98890f7462917..87e9734ea1054 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js @@ -21,7 +21,8 @@ import { buildProcessorFunction } from '../build_processor_function'; import { processors } from '../response_processors/table'; import { getLastValue } from '../../../../common/get_last_value'; import regression from 'regression'; -import { first, get, set } from 'lodash'; +import { first, get } from 'lodash'; +import { overwrite } from '../helpers'; import { getActiveSeries } from '../helpers/get_active_series'; export function processBucket(panel) { @@ -35,7 +36,7 @@ export function processBucket(panel) { const timeseries = { buckets: get(bucket, `${series.id}.buckets`), }; - set(bucket, series.id, { meta, timeseries }); + overwrite(bucket, series.id, { meta, timeseries }); } const processor = buildProcessorFunction(processors, bucket, panel, series); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 8175361ffc842..b8069b31257d3 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -206,17 +206,15 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async getFieldsTabCount() { return retry.try(async () => { - const indexedFieldsTab = await find.byCssSelector('#indexedFields .euiTab__content'); - const text = await indexedFieldsTab.getVisibleText(); - return text.split(/[()]/)[1]; + const text = await testSubjects.getVisibleText('tab-indexedFields'); + return text.split(' ')[1].replace(/\((.*)\)/, '$1'); }); } async getScriptedFieldsTabCount() { return await retry.try(async () => { - const scriptedFieldsTab = await find.byCssSelector('#scriptedFields .euiTab__content'); - const text = await scriptedFieldsTab.getVisibleText(); - return text.split(/[()]/)[1]; + const text = await testSubjects.getVisibleText('tab-scriptedFields'); + return text.split(' ')[2].replace(/\((.*)\)/, '$1'); }); } @@ -431,17 +429,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickFieldsTab() { log.debug('click Fields tab'); - await find.clickByCssSelector('#indexedFields'); + await testSubjects.click('tab-indexedFields'); } async clickScriptedFieldsTab() { log.debug('click Scripted Fields tab'); - await find.clickByCssSelector('#scriptedFields'); + await testSubjects.click('tab-scriptedFields'); } async clickSourceFiltersTab() { log.debug('click Source Filters tab'); - await find.clickByCssSelector('#sourceFilters'); + await testSubjects.click('tab-sourceFilters'); } async editScriptedField(name: string) { diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index dc457c38a52af..87db0005fb656 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -5,22 +5,23 @@ */ import { i18n } from '@kbn/i18n'; +import cytoscape from 'cytoscape'; import { ILicense } from '../../licensing/public'; import { AGENT_NAME, SERVICE_ENVIRONMENT, SERVICE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, SPAN_SUBTYPE, - SPAN_TYPE, - SPAN_DESTINATION_SERVICE_RESOURCE + SPAN_TYPE } from './elasticsearch_fieldnames'; -export interface ServiceConnectionNode { +export interface ServiceConnectionNode extends cytoscape.NodeDataDefinition { [SERVICE_NAME]: string; [SERVICE_ENVIRONMENT]: string | null; [AGENT_NAME]: string; } -export interface ExternalConnectionNode { +export interface ExternalConnectionNode extends cytoscape.NodeDataDefinition { [SPAN_DESTINATION_SERVICE_RESOURCE]: string; [SPAN_TYPE]: string; [SPAN_SUBTYPE]: string; diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 7d5f0a75d2208..8fb44b70bc081 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -9,16 +9,15 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; +import { getMlIndex } from '../../../common/ml_job_constants'; import { getServicesProjection } from '../../../common/projections/services'; import { mergeProjection } from '../../../common/projections/util/merge_projection'; import { PromiseReturnType } from '../../../typings/common'; +import { rangeFilter } from '../helpers/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { dedupeConnections } from './dedupe_connections'; +import { transformServiceMapResponses } from './transform_service_map_responses'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; -import { addAnomaliesToServicesData } from './ml_helpers'; -import { getMlIndex } from '../../../common/ml_job_constants'; -import { rangeFilter } from '../helpers/range_filter'; export interface IEnvOptions { setup: Setup & SetupTimeRange; @@ -179,13 +178,9 @@ export async function getServiceMap(options: IEnvOptions) { getAnomaliesData(options) ]); - const servicesDataWithAnomalies = addAnomaliesToServicesData( - servicesData, - anomaliesData - ); - - return dedupeConnections({ + return transformServiceMapResponses({ ...connectionData, - services: servicesDataWithAnomalies + anomalies: anomaliesData, + services: servicesData }); } diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts index c80ba8dba01ea..908dbe6df4636 100644 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.test.ts @@ -5,11 +5,11 @@ */ import { AnomaliesResponse } from './get_service_map'; -import { addAnomaliesToServicesData } from './ml_helpers'; +import { addAnomaliesDataToNodes } from './ml_helpers'; -describe('addAnomaliesToServicesData', () => { - it('adds anomalies to services data', () => { - const servicesData = [ +describe('addAnomaliesDataToNodes', () => { + it('adds anomalies to nodes', () => { + const nodes = [ { 'service.name': 'opbeans-ruby', 'agent.name': 'ruby', @@ -89,8 +89,8 @@ describe('addAnomaliesToServicesData', () => { ]; expect( - addAnomaliesToServicesData( - servicesData, + addAnomaliesDataToNodes( + nodes, (anomaliesResponse as unknown) as AnomaliesResponse ) ).toEqual(result); diff --git a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts index 9789911660bd0..fae9e7d4cb1c6 100644 --- a/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts +++ b/x-pack/plugins/apm/server/lib/service_map/ml_helpers.ts @@ -9,10 +9,11 @@ import { getMlJobServiceName, getSeverity } from '../../../common/ml_job_constants'; -import { AnomaliesResponse, ServicesResponse } from './get_service_map'; +import { ConnectionNode } from '../../../common/service_map'; +import { AnomaliesResponse } from './get_service_map'; -export function addAnomaliesToServicesData( - servicesData: ServicesResponse, +export function addAnomaliesDataToNodes( + nodes: ConnectionNode[], anomaliesResponse: AnomaliesResponse ) { const anomaliesMap = ( @@ -52,7 +53,7 @@ export function addAnomaliesToServicesData( }; }, {}); - const servicesDataWithAnomalies = servicesData.map(service => { + const servicesDataWithAnomalies = nodes.map(service => { const serviceAnomalies = anomaliesMap[service[SERVICE_NAME]]; if (serviceAnomalies) { const maxScore = serviceAnomalies.max_score; diff --git a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts similarity index 83% rename from x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts rename to x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 4af8a54139204..45b64c1ad03a4 100644 --- a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -4,16 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ServiceMapResponse } from './'; import { - SPAN_DESTINATION_SERVICE_RESOURCE, - SERVICE_NAME, - SERVICE_ENVIRONMENT, AGENT_NAME, - SPAN_TYPE, - SPAN_SUBTYPE -} from '../../../../common/elasticsearch_fieldnames'; -import { dedupeConnections } from './'; + SERVICE_ENVIRONMENT, + SERVICE_NAME, + SPAN_DESTINATION_SERVICE_RESOURCE, + SPAN_SUBTYPE, + SPAN_TYPE +} from '../../../common/elasticsearch_fieldnames'; +import { AnomaliesResponse } from './get_service_map'; +import { + transformServiceMapResponses, + ServiceMapResponse +} from './transform_service_map_responses'; const nodejsService = { [SERVICE_NAME]: 'opbeans-node', @@ -33,9 +36,14 @@ const javaService = { [AGENT_NAME]: 'java' }; -describe('dedupeConnections', () => { +const anomalies = ({ + aggregations: { jobs: { buckets: [] } } +} as unknown) as AnomaliesResponse; + +describe('transformServiceMapResponses', () => { it('maps external destinations to internal services', () => { const response: ServiceMapResponse = { + anomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -51,7 +59,7 @@ describe('dedupeConnections', () => { ] }; - const { elements } = dedupeConnections(response); + const { elements } = transformServiceMapResponses(response); const connection = elements.find( element => 'source' in element.data && 'target' in element.data @@ -67,6 +75,7 @@ describe('dedupeConnections', () => { it('collapses external destinations based on span.destination.resource.name', () => { const response: ServiceMapResponse = { + anomalies, services: [nodejsService, javaService], discoveredServices: [ { @@ -89,7 +98,7 @@ describe('dedupeConnections', () => { ] }; - const { elements } = dedupeConnections(response); + const { elements } = transformServiceMapResponses(response); const connections = elements.filter(element => 'source' in element.data); @@ -102,6 +111,7 @@ describe('dedupeConnections', () => { it('picks the first span.type/subtype in an alphabetically sorted list', () => { const response: ServiceMapResponse = { + anomalies, services: [javaService], discoveredServices: [], connections: [ @@ -126,7 +136,7 @@ describe('dedupeConnections', () => { ] }; - const { elements } = dedupeConnections(response); + const { elements } = transformServiceMapResponses(response); const nodes = elements.filter(element => !('source' in element.data)); @@ -140,6 +150,7 @@ describe('dedupeConnections', () => { it('processes connections without a matching "service" aggregation', () => { const response: ServiceMapResponse = { + anomalies, services: [javaService], discoveredServices: [], connections: [ @@ -150,7 +161,7 @@ describe('dedupeConnections', () => { ] }; - const { elements } = dedupeConnections(response); + const { elements } = transformServiceMapResponses(response); expect(elements.length).toBe(3); }); diff --git a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts similarity index 76% rename from x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts rename to x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index e5d7c0b2de10c..8b91bb98b5200 100644 --- a/x-pack/plugins/apm/server/lib/service_map/dedupe_connections/index.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -10,14 +10,19 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, SPAN_TYPE, SPAN_SUBTYPE -} from '../../../../common/elasticsearch_fieldnames'; +} from '../../../common/elasticsearch_fieldnames'; import { Connection, ConnectionNode, ServiceConnectionNode, ExternalConnectionNode -} from '../../../../common/service_map'; -import { ConnectionsResponse, ServicesResponse } from '../get_service_map'; +} from '../../../common/service_map'; +import { + ConnectionsResponse, + ServicesResponse, + AnomaliesResponse +} from './get_service_map'; +import { addAnomaliesDataToNodes } from './ml_helpers'; function getConnectionNodeId(node: ConnectionNode): string { if ('span.destination.service.resource' in node) { @@ -34,13 +39,16 @@ function getConnectionId(connection: Connection) { } export type ServiceMapResponse = ConnectionsResponse & { + anomalies: AnomaliesResponse; services: ServicesResponse; }; -export function dedupeConnections(response: ServiceMapResponse) { - const { discoveredServices, services, connections } = response; +export function transformServiceMapResponses(response: ServiceMapResponse) { + const { anomalies, discoveredServices, services, connections } = response; - const allNodes = connections + // Derive the rest of the map nodes from the connections and add the services + // from the services data query + const allNodes: ConnectionNode[] = connections .flatMap(connection => [connection.source, connection.destination]) .map(node => ({ ...node, id: getConnectionNodeId(node) })) .concat( @@ -50,25 +58,21 @@ export function dedupeConnections(response: ServiceMapResponse) { })) ); - const serviceNodes = allNodes.filter(node => SERVICE_NAME in node) as Array< - ServiceConnectionNode & { - id: string; - } - >; + // List of nodes that are services + const serviceNodes = allNodes.filter( + node => SERVICE_NAME in node + ) as ServiceConnectionNode[]; + // List of nodes that are externals const externalNodes = allNodes.filter( node => SPAN_DESTINATION_SERVICE_RESOURCE in node - ) as Array< - ExternalConnectionNode & { - id: string; - } - >; + ) as ExternalConnectionNode[]; - // 1. maps external nodes to internal services - // 2. collapses external nodes into one node based on span.destination.service.resource - // 3. picks the first available span.type/span.subtype in an alphabetically sorted list + // 1. Map external nodes to internal services + // 2. Collapse external nodes into one node based on span.destination.service.resource + // 3. Pick the first available span.type/span.subtype in an alphabetically sorted list const nodeMap = allNodes.reduce((map, node) => { - if (map[node.id]) { + if (!node.id || map[node.id]) { return map; } @@ -119,14 +123,14 @@ export function dedupeConnections(response: ServiceMapResponse) { .sort()[0] } }; - }, {} as Record); + }, {} as Record); - // maps destination.address to service.name if possible + // Map destination.address to service.name if possible function getConnectionNode(node: ConnectionNode) { return nodeMap[getConnectionNodeId(node)]; } - // build connections with mapped nodes + // Build connections with mapped nodes const mappedConnections = connections .map(connection => { const sourceData = getConnectionNode(connection.source); @@ -166,7 +170,7 @@ export function dedupeConnections(response: ServiceMapResponse) { {} as Record ); - // instead of adding connections in two directions, + // Instead of adding connections in two directions, // we add a `bidirectional` flag to use in styling const dedupedConnections = (sortBy( Object.values(connectionsById), @@ -192,10 +196,18 @@ export function dedupeConnections(response: ServiceMapResponse) { return prev.concat(connection); }, []); + // Add anomlies data + const dedupedNodesWithAnomliesData = addAnomaliesDataToNodes( + dedupedNodes, + anomalies + ); + // Put everything together in elements, with everything in the "data" property - const elements = [...dedupedConnections, ...dedupedNodes].map(element => ({ - data: element - })); + const elements = [...dedupedConnections, ...dedupedNodesWithAnomliesData].map( + element => ({ + data: element + }) + ); return { elements }; } diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 82e751627a05b..77147d1b3b2b7 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -118,10 +118,7 @@ export const ExpressionChart: React.FC = ({ const series = { ...firstSeries, rows: firstSeries.rows.map(row => { - const newRow: MetricsExplorerRow = { - timestamp: row.timestamp, - metric_0: row.metric_0 || null, - }; + const newRow: MetricsExplorerRow = { ...row }; thresholds.forEach((thresholdValue, index) => { newRow[`metric_threshold_${index}`] = thresholdValue; }); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx index 8801df380b48d..be0f5f88a2b55 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_row.tsx @@ -234,4 +234,20 @@ export const aggregationType: { [key: string]: any } = { value: AGGREGATION_TYPES.SUM, validNormalizedTypes: ['number'], }, + p95: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.p95', { + defaultMessage: '95th Percentile', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.P95, + validNormalizedTypes: ['number'], + }, + p99: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.aggregationText.p99', { + defaultMessage: '99th Percentile', + }), + fieldRequired: false, + value: AGGREGATION_TYPES.P99, + validNormalizedTypes: ['number'], + }, }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts index af3baf191bed2..e421455cf6efd 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/types.ts @@ -29,6 +29,8 @@ export enum AGGREGATION_TYPES { MAX = 'max', RATE = 'rate', CARDINALITY = 'cardinality', + P95 = 'p95', + P99 = 'p99', } export interface MetricThresholdAlertParams { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 559422584f579..f773c843d12fd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -46,6 +46,24 @@ export const metricsExplorerMetricToTSVBMetric = (metric: MetricsExplorerOptions field: derivativeId, }, ]; + } else if (metric.aggregation === 'p95' || metric.aggregation === 'p99') { + const percentileValue = metric.aggregation === 'p95' ? '95' : '99'; + return [ + { + id: uuid.v1(), + type: 'percentile', + field: metric.field, + percentiles: [ + { + id: uuid.v1(), + value: percentileValue, + mode: 'line', + percentile: '', + shade: 0.2, + }, + ], + }, + ]; } else { return [ { diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx index 3b84fcbc34836..223318da8cf46 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/series_chart.tsx @@ -20,6 +20,7 @@ import { MetricsExplorerOptionsMetric, MetricsExplorerChartType, } from '../hooks/use_metrics_explorer_options'; +import { getMetricId } from './helpers/get_metric_id'; type NumberOrString = string | number; @@ -45,10 +46,12 @@ export const MetricsExplorerAreaChart = ({ metric, id, series, type, stack, opac colorTransformer(MetricsExplorerColor.color0); const yAccessors = Array.isArray(id) - ? id.map(i => `metric_${i}`).slice(id.length - 1, id.length) - : [`metric_${id}`]; + ? id.map(i => getMetricId(metric, i)).slice(id.length - 1, id.length) + : [getMetricId(metric, id)]; const y0Accessors = - Array.isArray(id) && id.length > 1 ? id.map(i => `metric_${i}`).slice(0, 1) : undefined; + Array.isArray(id) && id.length > 1 + ? id.map(i => getMetricId(metric, i)).slice(0, 1) + : undefined; const chartId = `series-${series.id}-${yAccessors.join('-')}`; const seriesAreaStyle: RecursivePartial = { @@ -85,8 +88,10 @@ export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => (metric.color && colorTransformer(metric.color)) || colorTransformer(MetricsExplorerColor.color0); - const yAccessor = `metric_${id}`; - const chartId = `series-${series.id}-${yAccessor}`; + const yAccessors = Array.isArray(id) + ? id.map(i => getMetricId(metric, i)).slice(id.length - 1, id.length) + : [getMetricId(metric, id)]; + const chartId = `series-${series.id}-${yAccessors.join('-')}`; const seriesBarStyle: RecursivePartial = { rectBorder: { @@ -100,13 +105,13 @@ export const MetricsExplorerBarChart = ({ metric, id, series, stack }: Props) => }; return ( { + const value = type === Aggregators.P95 ? 95 : 99; + return { + aggregatedValue: { + percentiles: { + field, + percents: [value], + keyed: false, + }, + }, + }; +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 2531e939792af..ed5efc1473953 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -233,6 +233,58 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); }); + describe('querying with the p99 aggregator', () => { + const instanceID = 'test-*'; + const execute = (comparator: Comparator, threshold: number[]) => + executor({ + services, + params: { + criteria: [ + { + ...baseCriterion, + comparator, + threshold, + aggType: 'p99', + metric: 'test.metric.2', + }, + ], + }, + }); + test('alerts based on the p99 values', async () => { + await execute(Comparator.GT, [1]); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + await execute(Comparator.LT, [1]); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); + }); + describe('querying with the p95 aggregator', () => { + const instanceID = 'test-*'; + const execute = (comparator: Comparator, threshold: number[]) => + executor({ + services, + params: { + criteria: [ + { + ...baseCriterion, + comparator, + threshold, + aggType: 'p95', + metric: 'test.metric.1', + }, + ], + }, + }); + test('alerts based on the p95 values', async () => { + await execute(Comparator.GT, [0.25]); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + await execute(Comparator.LT, [0.95]); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); + }); describe("querying a metric that hasn't reported data", () => { const instanceID = 'test-*'; const execute = (alertOnNoData: boolean) => diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index ec9389537835b..71bee3209bf53 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { mapValues } from 'lodash'; +import { mapValues, first } from 'lodash'; import { i18n } from '@kbn/i18n'; import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types'; import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; @@ -21,12 +21,16 @@ import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/ser import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; import { InfraBackendLibs } from '../../infra_types'; +import { createPercentileAggregation } from './create_percentile_aggregation'; const TOTAL_BUCKETS = 5; interface Aggregation { aggregatedIntervals: { - buckets: Array<{ aggregatedValue: { value: number }; doc_count: number }>; + buckets: Array<{ + aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> }; + doc_count: number; + }>; }; } @@ -47,6 +51,12 @@ const getCurrentValueFromAggregations = ( if (aggType === Aggregators.COUNT) { return mostRecentBucket.doc_count; } + if (aggType === Aggregators.P95 || aggType === Aggregators.P99) { + const values = mostRecentBucket.aggregatedValue?.values || []; + const firstValue = first(values); + if (!firstValue) return null; + return firstValue.value; + } const { value } = mostRecentBucket.aggregatedValue; return value; } catch (e) { @@ -86,6 +96,8 @@ export const getElasticsearchMetricQuery = ( ? {} : aggType === Aggregators.RATE ? networkTraffic('aggregatedValue', metric) + : aggType === Aggregators.P95 || aggType === Aggregators.P99 + ? createPercentileAggregation(aggType, metric) : { aggregatedValue: { [aggType]: { @@ -275,7 +287,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s ); // Because each alert result has the same group definitions, just grap the groups from the first one. - const groups = Object.keys(alertResults[0]); + const groups = Object.keys(first(alertResults)); for (const group of groups) { const alertInstance = services.alertInstanceFactory(`${alertId}-${group}`); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts index fa55f80e472de..25b709d6afc51 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts @@ -7,22 +7,22 @@ const bucketsA = [ { doc_count: 2, - aggregatedValue: { value: 0.5 }, + aggregatedValue: { value: 0.5, values: [{ key: 95.0, value: 0.5 }] }, }, { doc_count: 3, - aggregatedValue: { value: 1.0 }, + aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] }, }, ]; const bucketsB = [ { doc_count: 4, - aggregatedValue: { value: 2.5 }, + aggregatedValue: { value: 2.5, values: [{ key: 99.0, value: 2.5 }] }, }, { doc_count: 5, - aggregatedValue: { value: 3.5 }, + aggregatedValue: { value: 3.5, values: [{ key: 99.0, value: 3.5 }] }, }, ]; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index 18f5503fe2c9e..76ddd107bd728 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -23,6 +23,8 @@ export enum Aggregators { MAX = 'max', RATE = 'rate', CARDINALITY = 'cardinality', + P95 = 'p95', + P99 = 'p99', } export enum AlertStates { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index f7b0e726ecc53..fa36a0626d632 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -165,6 +165,15 @@ export function extractJobDetails(job) { items: filterObjects(job.model_size_stats).map(formatValues), }; + const jobTimingStats = { + id: 'jobTimingStats', + title: i18n.translate('xpack.ml.jobsList.jobDetails.jobTimingStatsTitle', { + defaultMessage: 'Job timing stats', + }), + position: 'left', + items: filterObjects(job.timing_stats).map(formatValues), + }; + const datafeedTimingStats = { id: 'datafeedTimingStats', title: i18n.translate('xpack.ml.jobsList.jobDetails.datafeedTimingStatsTitle', { @@ -192,6 +201,7 @@ export function extractJobDetails(job) { datafeed, counts, modelSizeStats, + jobTimingStats, datafeedTimingStats, }; } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index 9984f3be299ae..246a476517ace 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -63,6 +63,12 @@ export function formatValues([key, value]) { // numbers rounded to 3 decimal places case 'average_search_time_per_bucket_ms': case 'exponential_average_search_time_per_hour_ms': + case 'total_bucket_processing_time_ms': + case 'minimum_bucket_processing_time_ms': + case 'maximum_bucket_processing_time_ms': + case 'average_bucket_processing_time_ms': + case 'exponential_average_bucket_processing_time_ms': + case 'exponential_average_bucket_processing_time_per_hour_ms': value = typeof value === 'number' ? roundToDecimalPlace(value, 3).toLocaleString() : value; break; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index e3f348ad32b0c..0375997b86bb8 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -60,6 +60,7 @@ export class JobDetails extends Component { datafeed, counts, modelSizeStats, + jobTimingStats, datafeedTimingStats, } = extractJobDetails(job); @@ -102,7 +103,7 @@ export class JobDetails extends Component { content: ( ), }, diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index bbfec49ac1388..fb75476c48fa3 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -369,6 +369,8 @@ class JobService { delete tempJob.open_time; delete tempJob.established_model_memory; delete tempJob.calendars; + delete tempJob.timing_stats; + delete tempJob.forecasts_stats; delete tempJob.analysis_config.use_per_partition_normalization; diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 6024ecf4925e6..225cd43e411a4 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -328,7 +328,7 @@ export function jobsProvider(callAsCurrentUser: APICaller) { // create jobs objects containing job stats, datafeeds, datafeed stats and calendars if (jobResults && jobResults.jobs) { jobResults.jobs.forEach(job => { - const tempJob = job as CombinedJobWithStats; + let tempJob = job as CombinedJobWithStats; const calendars: string[] = [ ...(calendarsByJobId[tempJob.job_id] || []), @@ -341,9 +341,7 @@ export function jobsProvider(callAsCurrentUser: APICaller) { if (jobStatsResults && jobStatsResults.jobs) { const jobStats = jobStatsResults.jobs.find(js => js.job_id === tempJob.job_id); if (jobStats !== undefined) { - tempJob.state = jobStats.state; - tempJob.data_counts = jobStats.data_counts; - tempJob.model_size_stats = jobStats.model_size_stats; + tempJob = { ...tempJob, ...jobStats }; if (jobStats.node) { tempJob.node = jobStats.node; } diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts index 9851f80a42d5b..ca127f43d08af 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.test.ts @@ -290,7 +290,8 @@ describe('ML - validateJob', () => { }); }); - it('basic validation passes, extended checks return some messages', () => { + // Failing https://github.com/elastic/kibana/issues/65865 + it.skip('basic validation passes, extended checks return some messages', () => { const payload = getBasicPayload(); return validateJob(callWithRequest, payload).then(messages => { const ids = messages.map(m => m.id); @@ -303,7 +304,8 @@ describe('ML - validateJob', () => { }); }); - it('categorization job using mlcategory passes aggregatable field check', () => { + // Failing https://github.com/elastic/kibana/issues/65866 + it.skip('categorization job using mlcategory passes aggregatable field check', () => { const payload: any = { job: { job_id: 'categorization_test', @@ -369,7 +371,8 @@ describe('ML - validateJob', () => { }); }); - it('script field not reported as non aggregatable', () => { + // Failing https://github.com/elastic/kibana/issues/65867 + it.skip('script field not reported as non aggregatable', () => { const payload: any = { job: { job_id: 'categorization_test', diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx index b48cc546fe78c..7c9accd4cef49 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx +++ b/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx @@ -7,6 +7,7 @@ import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiToolTip } from '@elastic/eui'; import { isString, isEmpty } from 'lodash/fp'; import React from 'react'; +import styled from 'styled-components'; import { DefaultDraggable } from '../../../draggables'; import { getEmptyTagValue } from '../../../empty_value'; @@ -18,6 +19,10 @@ import endPointSvg from '../../../../utils/logo_endpoint/64_color.svg'; import * as i18n from './translations'; +const EventModuleFlexItem = styled(EuiFlexItem)` + width: 100%; +`; + export const renderRuleName = ({ contextId, eventId, @@ -87,7 +92,7 @@ export const renderEventModule = ({ endpointRefUrl != null && !isEmpty(endpointRefUrl) ? 'flexStart' : 'spaceBetween' } > - + {content} - + {endpointRefUrl != null && canYouAddEndpointLogo(moduleName, endpointRefUrl) && ( ( const popover = useMemo(() => { return ( - ( panelPaddingSize={!alwaysShow ? 's' : 'none'} > {isOpen ? hoverContent : null} - + ); }, [content, onMouseLeave, isOpen, alwaysShow, hoverContent]); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts index 80594ca74a353..30362392898d1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.test.ts @@ -7,10 +7,18 @@ import { getSignalsTemplate } from './get_signals_template'; describe('get_signals_template', () => { - test('it should set the lifecycle name and the rollover alias to be the name of the index passed in', () => { + test('it should set the lifecycle "name" and "rollover_alias" to be the name of the index passed in', () => { const template = getSignalsTemplate('test-index'); expect(template.settings).toEqual({ - index: { lifecycle: { name: 'test-index', rollover_alias: 'test-index' } }, + index: { + lifecycle: { + name: 'test-index', + rollover_alias: 'test-index', + }, + }, + mapping: { + total_fields: { limit: 10000 }, + }, }); }); @@ -28,4 +36,9 @@ describe('get_signals_template', () => { const template = getSignalsTemplate('test-index'); expect(typeof template.mappings.properties.signal).toEqual('object'); }); + + test('it should have a "total_fields" section that is at least 10k in size', () => { + const template = getSignalsTemplate('test-index'); + expect(template.settings.mapping.total_fields.limit).toBeGreaterThanOrEqual(10000); + }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts index c6580f0bdda42..01d7182e253ce 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -17,6 +17,11 @@ export const getSignalsTemplate = (index: string) => { rollover_alias: index, }, }, + mapping: { + total_fields: { + limit: 10000, + }, + }, }, index_patterns: [`${index}-*`], mappings: ecsMapping.mappings, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 3440bb28b2468..8511ab468ca80 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -127,26 +127,27 @@ export const AlertDetails: React.FunctionComponent = ({ defaultMessage="Edit" /> - - - + {editFlyoutVisible && ( + + setEditFlyoutVisibility(false)} + /> + + )} ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 722db146a54ce..4d8801d8b7484 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -131,11 +131,7 @@ describe('alert_edit', () => { capabilities: deps!.capabilities, }} > - {}} - initialAlert={alert} - /> + {}} initialAlert={alert} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 00bc9874face1..747464d2212f4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -31,15 +31,10 @@ import { PLUGIN } from '../../constants/plugin'; interface AlertEditProps { initialAlert: Alert; - editFlyoutVisible: boolean; - setEditFlyoutVisibility: React.Dispatch>; + onClose(): void; } -export const AlertEdit = ({ - initialAlert, - editFlyoutVisible, - setEditFlyoutVisibility, -}: AlertEditProps) => { +export const AlertEdit = ({ initialAlert, onClose }: AlertEditProps) => { const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); const [isSaving, setIsSaving] = useState(false); const [hasActionsDisabled, setHasActionsDisabled] = useState(false); @@ -57,14 +52,10 @@ export const AlertEdit = ({ } = useAlertsContext(); const closeFlyout = useCallback(() => { - setEditFlyoutVisibility(false); + onClose(); setAlert('alert', initialAlert); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [setEditFlyoutVisibility]); - - if (!editFlyoutVisible) { - return null; - } + }, [onClose]); const alertType = alertTypeRegistry.get(alert.alertTypeId); diff --git a/x-pack/plugins/uptime/common/translations.ts b/x-pack/plugins/uptime/common/translations.ts new file mode 100644 index 0000000000000..678fe7cb1f984 --- /dev/null +++ b/x-pack/plugins/uptime/common/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const VALUE_MUST_BE_GREATER_THEN_ZEO = i18n.translate( + 'xpack.uptime.settings.invalid.error', + { + defaultMessage: 'Value must be greater than 0.', + } +); diff --git a/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx index 5bfd26f7c6401..8200e8292cd98 100644 --- a/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx +++ b/x-pack/plugins/uptime/public/components/settings/certificate_form.tsx @@ -59,7 +59,7 @@ export const CertificateExpirationForm: React.FC = ({ > = ({ }} /> } - isInvalid={!!fieldErrors?.certificatesThresholds?.expirationThresholdError} + isInvalid={!!fieldErrors?.expirationThresholdError} label={ = ({ = ({ = ({ }} /> } - isInvalid={!!fieldErrors?.certificatesThresholds?.ageThresholdError} + isInvalid={!!fieldErrors?.ageThresholdError} label={ = ({ + onChange={({ currentTarget: { value } }) => onChange({ - certAgeThreshold: Number(e.currentTarget.value), + certAgeThreshold: Number(value), }) } /> diff --git a/x-pack/plugins/uptime/public/pages/settings.tsx b/x-pack/plugins/uptime/public/pages/settings.tsx index 7ed9cf4444343..d018567ae1104 100644 --- a/x-pack/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/plugins/uptime/public/pages/settings.tsx @@ -32,13 +32,12 @@ import { OnFieldChangeType, } from '../components/settings/certificate_form'; import * as Translations from './translations'; +import { VALUE_MUST_BE_GREATER_THEN_ZEO } from '../../common/translations'; interface SettingsPageFieldErrors { - heartbeatIndices: 'May not be blank' | ''; - certificatesThresholds: { - expirationThresholdError: string | null; - ageThresholdError: string | null; - } | null; + heartbeatIndices: string | ''; + expirationThresholdError?: string; + ageThresholdError?: string; } export interface SettingsFormProps { @@ -49,22 +48,28 @@ export interface SettingsFormProps { isDisabled: boolean; } +const isValidCertVal = (val: string | number) => { + if (val === '') { + return Translations.BLANK_STR; + } + if (val === 0) { + return VALUE_MUST_BE_GREATER_THEN_ZEO; + } +}; + const getFieldErrors = (formFields: DynamicSettings | null): SettingsPageFieldErrors | null => { if (formFields) { - const blankStr = 'May not be blank'; const { certAgeThreshold, certExpirationThreshold, heartbeatIndices } = formFields; - const heartbeatIndErr = heartbeatIndices.match(/^\S+$/) ? '' : blankStr; - const expirationThresholdError = certExpirationThreshold ? null : blankStr; - const ageThresholdError = certAgeThreshold ? null : blankStr; + + const indError = heartbeatIndices.match(/^\S+$/) ? '' : Translations.BLANK_STR; + + const expError = isValidCertVal(certExpirationThreshold); + const ageError = isValidCertVal(certAgeThreshold); + return { - heartbeatIndices: heartbeatIndErr, - certificatesThresholds: - expirationThresholdError || ageThresholdError - ? { - expirationThresholdError, - ageThresholdError, - } - : null, + heartbeatIndices: indError, + expirationThresholdError: expError, + ageThresholdError: ageError, }; } return null; diff --git a/x-pack/plugins/uptime/public/pages/translations.ts b/x-pack/plugins/uptime/public/pages/translations.ts index 74fb2eeb1416b..8ed5503235884 100644 --- a/x-pack/plugins/uptime/public/pages/translations.ts +++ b/x-pack/plugins/uptime/public/pages/translations.ts @@ -36,3 +36,7 @@ export const settings = { defaultMessage: 'Return to overview', }), }; + +export const BLANK_STR = i18n.translate('xpack.uptime.settings.blank.error', { + defaultMessage: 'May not be blank.', +}); diff --git a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts index 31833a25ee8ac..c7d532d932aa6 100644 --- a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts +++ b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts @@ -11,6 +11,7 @@ import { UMServerLibs } from '../lib/lib'; import { DynamicSettings, DynamicSettingsType } from '../../common/runtime_types'; import { UMRestApiRouteFactory } from '.'; import { savedObjectsAdapter } from '../lib/saved_objects'; +import { VALUE_MUST_BE_GREATER_THEN_ZEO } from '../../common/translations'; export const createGetDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', @@ -23,19 +24,35 @@ export const createGetDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMSer }, }); +const validateCertsValues = (settings: DynamicSettings) => { + const errors: any = {}; + if (settings.certAgeThreshold <= 0) { + errors.certAgeThreshold = VALUE_MUST_BE_GREATER_THEN_ZEO; + } + if (settings.certExpirationThreshold <= 0) { + errors.certExpirationThreshold = VALUE_MUST_BE_GREATER_THEN_ZEO; + } + if (errors.certAgeThreshold || errors.certExpirationThreshold) { + return errors; + } +}; + export const createPostDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'POST', path: '/api/uptime/dynamic_settings', validate: { - body: schema.object({}, { unknowns: 'allow' }), + body: schema.object({ + heartbeatIndices: schema.string(), + certAgeThreshold: schema.number(), + certExpirationThreshold: schema.number(), + }), }, writeAccess: true, - options: { - tags: ['access:uptime-write'], - }, handler: async ({ savedObjectsClient }, _context, request, response): Promise => { const decoded = DynamicSettingsType.decode(request.body); - if (isRight(decoded)) { + const certThresholdErrors = validateCertsValues(request.body as DynamicSettings); + + if (isRight(decoded) && !certThresholdErrors) { const newSettings: DynamicSettings = decoded.right; await savedObjectsAdapter.setUptimeDynamicSettings(savedObjectsClient, newSettings); @@ -47,7 +64,7 @@ export const createPostDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMSe } else { const error = PathReporter.report(decoded).join(', '); return response.badRequest({ - body: error, + body: JSON.stringify(certThresholdErrors) || error, }); } }, diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts index a9fc2706a6ba2..930cf42b9efc5 100644 --- a/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts @@ -18,7 +18,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); describe('get_configure', () => { afterEach(async () => { diff --git a/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts index d66baa2a2eee2..5f7d7c4c5c346 100644 --- a/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts @@ -18,7 +18,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); describe('post_configure', () => { afterEach(async () => { diff --git a/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts index c2284492e5b77..86214a2bd3187 100644 --- a/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts @@ -18,7 +18,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const es = getService('legacyEs'); + const es = getService('es'); describe('post_configure', () => { afterEach(async () => { diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 6d0db69309b90..df768ff09b368 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Client } from '@elastic/elasticsearch'; import { CasesConfigureRequest, CasesConfigureResponse } from '../../../../plugins/case/common/api'; export const getConfiguration = (connector_id: string = 'connector-1'): CasesConfigureRequest => { @@ -29,12 +30,12 @@ export const removeServerGeneratedPropertiesFromConfigure = ( return rest; }; -export const deleteConfiguration = async (es: any): Promise => { +export const deleteConfiguration = async (es: Client): Promise => { await es.deleteByQuery({ index: '.kibana', q: 'type:cases-configure', - waitForCompletion: true, - refresh: 'wait_for', + wait_for_completion: true, + refresh: true, body: {}, }); }; diff --git a/yarn.lock b/yarn.lock index 1ce919e210e66..8b16bac98e895 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19197,10 +19197,10 @@ less-loader@5.0.0: loader-utils "^1.1.0" pify "^4.0.1" -less@^2.7.3: - version "2.7.3" - resolved "https://registry.yarnpkg.com/less/-/less-2.7.3.tgz#cc1260f51c900a9ec0d91fb6998139e02507b63b" - integrity sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ== +"less@npm:@elastic/less@2.7.3-kibana": + version "2.7.3-kibana" + resolved "https://registry.yarnpkg.com/@elastic/less/-/less-2.7.3-kibana.tgz#3de5e0b06bb095b1cc1149043d67f8dc36272d23" + integrity sha512-Okm31ZKE28/m3bH0h0mNpQH0zqVWNFqRKDlsBd1AYHGdM1yBq4mzeO6IRUykB81XDGlqL0m4ThSA7mc3hy+LVg== optionalDependencies: errno "^0.1.1" graceful-fs "^4.1.2"