From 1240d02c9d72bbedbd8b24aefecf5a54772d109a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:08:48 +0100 Subject: [PATCH 01/30] [APM] Make it possible to link directly to a trace with just the trace.id (#51450) * create new api to fetch root transaction by trace id * redirecting trace to transaction * redirecting trace to transaction * redirecting trace to transaction * testing external link * testing external link * testing external link * testing external link * changing route name * refactoring * refactoring * refactoring * fixing merge conflicts * adding rangeFrom and to, into the url query param * removing convertedValue from duration formatter * refactoring ES query and tracelink component * pr comments * refactoring link --- .../apm/common/elasticsearch_fieldnames.ts | 1 + .../app/Main/route_config/index.tsx | 8 ++ .../app/Main/route_config/route_names.tsx | 3 +- .../app/TraceLink/__test__/TraceLink.test.tsx | 84 ++++++++++++++++ .../public/components/app/TraceLink/index.tsx | 96 +++++++++++++++++++ .../Waterfall/WaterfallItem.tsx | 3 +- .../shared/Links/apm/ExternalLinks.test.ts | 26 +++++ .../shared/Links/apm/ExternalLinks.ts | 21 ++++ .../context/UrlParamsContext/helpers.ts | 12 ++- .../UrlParamsContext/resolveUrlParams.ts | 4 +- .../public/context/UrlParamsContext/types.ts | 1 + x-pack/legacy/plugins/apm/public/index.scss | 1 + .../apm/public/new-platform/plugin.tsx | 1 + .../formatters/__test__/datetime.test.ts | 52 ++++++++-- .../apm/public/utils/formatters/datetime.ts | 23 +++-- .../lib/transactions/get_transaction/index.ts | 3 +- .../get_transaction_by_trace/index.ts | 51 ++++++++++ .../apm/server/routes/create_apm_api.ts | 6 +- .../plugins/apm/server/routes/transaction.ts | 24 +++++ 19 files changed, 398 insertions(+), 22 deletions(-) create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts create mode 100644 x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts create mode 100644 x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts create mode 100644 x-pack/legacy/plugins/apm/server/routes/transaction.ts diff --git a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts index d0830337e0d351..b8b35e79a9908b 100644 --- a/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/legacy/plugins/apm/common/elasticsearch_fieldnames.ts @@ -3,6 +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. */ + export const SERVICE_NAME = 'service.name'; export const SERVICE_ENVIRONMENT = 'service.environment'; export const SERVICE_AGENT_NAME = 'agent.name'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx index a410febce56951..ba2786ebf64de2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -21,6 +21,7 @@ import { toQuery } from '../../../shared/Links/url_helpers'; import { ServiceNodeMetrics } from '../../ServiceNodeMetrics'; import { resolveUrlParams } from '../../../../context/UrlParamsContext/resolveUrlParams'; import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; +import { TraceLink } from '../../TraceLink'; const metricsBreadcrumb = i18n.translate('xpack.apm.breadcrumb.metricsTitle', { defaultMessage: 'Metrics' @@ -190,6 +191,13 @@ export function getRoutes({ return query.transactionName as string; }, name: RouteName.TRANSACTION_NAME + }, + { + exact: true, + path: '/link-to/trace/:traceId', + component: TraceLink, + breadcrumb: null, + name: RouteName.LINK_TO_TRACE } ]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx index ab02e38ee9c9d9..0ae7a948be4e18 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/route_names.tsx @@ -21,5 +21,6 @@ export enum RouteName { SETTINGS = 'settings', AGENT_CONFIGURATION = 'agent_configuration', INDICES = 'indices', - SERVICE_NODES = 'nodes' + SERVICE_NODES = 'nodes', + LINK_TO_TRACE = 'link_to_trace' } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx new file mode 100644 index 00000000000000..b77524fca050dc --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { shallow } from 'enzyme'; +import * as urlParamsHooks from '../../../../hooks/useUrlParams'; +import * as hooks from '../../../../hooks/useFetcher'; +import { TraceLink } from '../'; + +jest.mock('../../Main/route_config/index.tsx', () => ({ + routes: [ + { + path: '/services/:serviceName/transactions/view', + name: 'transaction_name' + }, + { + path: '/traces', + name: 'traces' + } + ] +})); + +describe('TraceLink', () => { + afterAll(() => { + jest.clearAllMocks(); + }); + it('renders transition page', () => { + const component = render(); + expect(component.getByText('Fetching trace...')).toBeDefined(); + }); + + it('renders trace page when transaction is not found', () => { + spyOn(urlParamsHooks, 'useUrlParams').and.returnValue({ + urlParams: { + traceIdLink: '123', + rangeFrom: 'now-24h', + rangeTo: 'now' + } + }); + spyOn(hooks, 'useFetcher').and.returnValue({ + data: { transaction: undefined }, + status: 'success' + }); + + const component = shallow(); + expect(component.prop('to')).toEqual( + '/traces?kuery=trace.id%2520%253A%2520%2522123%2522&rangeFrom=now-24h&rangeTo=now' + ); + }); + + describe('transaction page', () => { + beforeAll(() => { + spyOn(urlParamsHooks, 'useUrlParams').and.returnValue({ + urlParams: { + traceIdLink: '123', + rangeFrom: 'now-24h', + rangeTo: 'now' + } + }); + }); + it('renders with date range params', () => { + const transaction = { + service: { name: 'foo' }, + transaction: { + id: '456', + name: 'bar', + type: 'GET' + }, + trace: { id: 123 } + }; + spyOn(hooks, 'useFetcher').and.returnValue({ + data: { transaction }, + status: 'success' + }); + const component = shallow(); + expect(component.prop('to')).toEqual( + '/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now' + ); + }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx new file mode 100644 index 00000000000000..b0489d58830d2b --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TraceLink/index.tsx @@ -0,0 +1,96 @@ +/* + * 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 { EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; +import { Redirect } from 'react-router-dom'; +import styled from 'styled-components'; +import url from 'url'; +import { TRACE_ID } from '../../../../common/elasticsearch_fieldnames'; +import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; +import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher'; +import { useUrlParams } from '../../../hooks/useUrlParams'; + +const CentralizedContainer = styled.div` + height: 100%; + display: flex; +`; + +const redirectToTransactionDetailPage = ({ + transaction, + rangeFrom, + rangeTo +}: { + transaction: Transaction; + rangeFrom?: string; + rangeTo?: string; +}) => + url.format({ + pathname: `/services/${transaction.service.name}/transactions/view`, + query: { + traceId: transaction.trace.id, + transactionId: transaction.transaction.id, + transactionName: transaction.transaction.name, + transactionType: transaction.transaction.type, + rangeFrom, + rangeTo + } + }); + +const redirectToTracePage = ({ + traceId, + rangeFrom, + rangeTo +}: { + traceId: string; + rangeFrom?: string; + rangeTo?: string; +}) => + url.format({ + pathname: `/traces`, + query: { + kuery: encodeURIComponent(`${TRACE_ID} : "${traceId}"`), + rangeFrom, + rangeTo + } + }); + +export const TraceLink = () => { + const { urlParams } = useUrlParams(); + const { traceIdLink: traceId, rangeFrom, rangeTo } = urlParams; + + const { data = { transaction: null }, status } = useFetcher( + callApmApi => { + if (traceId) { + return callApmApi({ + pathname: '/api/apm/transaction/{traceId}', + params: { + path: { + traceId + } + } + }); + } + }, + [traceId] + ); + if (traceId && status === FETCH_STATUS.SUCCESS) { + const to = data.transaction + ? redirectToTransactionDetailPage({ + transaction: data.transaction, + rangeFrom, + rangeTo + }) + : redirectToTracePage({ traceId, rangeFrom, rangeTo }); + return ; + } + + return ( + + Fetching trace...} /> + + ); +}; diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx index 40dc60c1efacd5..8d4fab4aa8dd91 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/WaterfallItem.tsx @@ -16,6 +16,7 @@ import { asDuration } from '../../../../../../utils/formatters'; import { ErrorCountBadge } from '../../ErrorCountBadge'; import { IWaterfallItem } from './waterfall_helpers/waterfall_helpers'; import { ErrorOverviewLink } from '../../../../../shared/Links/apm/ErrorOverviewLink'; +import { TRACE_ID } from '../../../../../../../common/elasticsearch_fieldnames'; type ItemType = 'transaction' | 'span'; @@ -212,7 +213,7 @@ export function WaterfallItem({ serviceName={item.transaction.service.name} query={{ kuery: encodeURIComponent( - `trace.id : "${item.transaction.trace.id}" and transaction.id : "${item.transaction.transaction.id}"` + `${TRACE_ID} : "${item.transaction.trace.id}" and transaction.id : "${item.transaction.transaction.id}"` ) }} color="danger" diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts new file mode 100644 index 00000000000000..3daeb3d09951dd --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.test.ts @@ -0,0 +1,26 @@ +/* + * 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 { getTraceUrl } from './ExternalLinks'; + +jest.mock('../../../app/Main/route_config/index.tsx', () => ({ + routes: [ + { + name: 'link_to_trace', + path: '/link-to/trace/:traceId' + } + ] +})); + +describe('ExternalLinks', () => { + afterAll(() => { + jest.clearAllMocks(); + }); + it('trace link', () => { + expect( + getTraceUrl({ traceId: 'foo', rangeFrom: '123', rangeTo: '456' }) + ).toEqual('/link-to/trace/foo?rangeFrom=123&rangeTo=456'); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts new file mode 100644 index 00000000000000..ff5e4744dc19d4 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/apm/ExternalLinks.ts @@ -0,0 +1,21 @@ +/* + * 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 url from 'url'; + +export const getTraceUrl = ({ + traceId, + rangeFrom, + rangeTo +}: { + traceId: string; + rangeFrom: string; + rangeTo: string; +}) => { + return url.format({ + pathname: `/link-to/trace/${traceId}`, + query: { rangeFrom, rangeTo } + }); +}; diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts index 1806e7395a8cce..f1e45fe45255d0 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/helpers.ts @@ -14,6 +14,7 @@ interface PathParams { serviceName?: string; errorGroupId?: string; serviceNodeName?: string; + traceId?: string; } export function getParsedDate(rawDate?: string, opts = {}) { @@ -67,7 +68,6 @@ export function removeUndefinedProps(obj: T): Partial { export function getPathParams(pathname: string = ''): PathParams { const paths = getPathAsArray(pathname); const pageName = paths[0]; - // TODO: use react router's real match params instead of guessing the path order switch (pageName) { @@ -115,6 +115,16 @@ export function getPathParams(pathname: string = ''): PathParams { return { processorEvent: ProcessorEvent.transaction }; + case 'link-to': + const link = paths[1]; + switch (link) { + case 'trace': + return { + traceId: paths[2] + }; + default: + return {}; + } default: return {}; } diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts index 7972bdbcf2ee63..887b45e73462c4 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/resolveUrlParams.ts @@ -30,7 +30,8 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { processorEvent, serviceName, serviceNodeName, - errorGroupId + errorGroupId, + traceId: traceIdLink } = getPathParams(location.pathname); const query = toQuery(location.search); @@ -87,6 +88,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { // path params processorEvent, serviceName, + traceIdLink, errorGroupId, serviceNodeName: serviceNodeName ? decodeURIComponent(serviceNodeName) diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts index 6b5b2d5341b842..496b28e168089a 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/types.ts @@ -32,4 +32,5 @@ export type IUrlParams = { serviceNodeName?: string; searchTerm?: string; processorEvent?: ProcessorEvent; + traceIdLink?: string; } & Partial>; diff --git a/x-pack/legacy/plugins/apm/public/index.scss b/x-pack/legacy/plugins/apm/public/index.scss index 2ad3995d05c032..04a070c304d6fa 100644 --- a/x-pack/legacy/plugins/apm/public/index.scss +++ b/x-pack/legacy/plugins/apm/public/index.scss @@ -12,4 +12,5 @@ .apmReactRoot { overflow-x: auto; + height: 100%; } diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index 6477f9a9dd41d1..b4eebc906e2c34 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -42,6 +42,7 @@ export const REACT_APP_ROOT_ID = 'react-apm-root'; const MainContainer = styled.main` min-width: ${px(unit * 50)}; padding: ${px(units.plus)}; + height: 100%; `; const App = ({ routes }: { routes: BreadcrumbRoute[] }) => { diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts index bec9cede00a2be..f9b5676111f72e 100644 --- a/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/__test__/datetime.test.ts @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ import moment from 'moment-timezone'; -import { asRelativeDateTimeRange, asAbsoluteDateTime } from '../datetime'; +import { + asRelativeDateTimeRange, + asAbsoluteDateTime, + getDateDifference +} from '../datetime'; describe('date time formatters', () => { + beforeAll(() => { + moment.tz.setDefault('Europe/Amsterdam'); + }); + afterAll(() => moment.tz.setDefault('')); describe('asRelativeDateTimeRange', () => { - beforeAll(() => { - moment.tz.setDefault('Europe/Amsterdam'); - }); - afterAll(() => moment.tz.setDefault('')); const formatDateToTimezone = (dateTimeString: string) => moment(dateTimeString).valueOf(); - describe('YYYY - YYYY', () => { it('range: 10 years', () => { const start = formatDateToTimezone('2000-01-01 10:01:01'); @@ -143,4 +146,41 @@ describe('date time formatters', () => { ); }); }); + describe('getDateDifference', () => { + it('milliseconds', () => { + const start = moment('2019-10-29 08:00:00.001'); + const end = moment('2019-10-29 08:00:00.005'); + expect(getDateDifference(start, end, 'milliseconds')).toEqual(4); + }); + it('seconds', () => { + const start = moment('2019-10-29 08:00:00'); + const end = moment('2019-10-29 08:00:10'); + expect(getDateDifference(start, end, 'seconds')).toEqual(10); + }); + it('minutes', () => { + const start = moment('2019-10-29 08:00:00'); + const end = moment('2019-10-29 08:15:00'); + expect(getDateDifference(start, end, 'minutes')).toEqual(15); + }); + it('hours', () => { + const start = moment('2019-10-29 08:00:00'); + const end = moment('2019-10-29 10:00:00'); + expect(getDateDifference(start, end, 'hours')).toEqual(2); + }); + it('days', () => { + const start = moment('2019-10-29 08:00:00'); + const end = moment('2019-10-30 10:00:00'); + expect(getDateDifference(start, end, 'days')).toEqual(1); + }); + it('months', () => { + const start = moment('2019-10-29 08:00:00'); + const end = moment('2019-12-29 08:00:00'); + expect(getDateDifference(start, end, 'months')).toEqual(2); + }); + it('years', () => { + const start = moment('2019-10-29 08:00:00'); + const end = moment('2020-10-29 08:00:00'); + expect(getDateDifference(start, end, 'years')).toEqual(1); + }); + }); }); diff --git a/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts index 98483a0351f069..591bbcaf342cff 100644 --- a/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts +++ b/x-pack/legacy/plugins/apm/public/utils/formatters/datetime.ts @@ -57,34 +57,37 @@ function getDateFormat(dateUnit: DateUnit) { } } +export const getDateDifference = ( + start: moment.Moment, + end: moment.Moment, + unitOfTime: DateUnit | TimeUnit +) => end.diff(start, unitOfTime); + function getFormatsAccordingToDateDifference( - momentStart: moment.Moment, - momentEnd: moment.Moment + start: moment.Moment, + end: moment.Moment ) { - const getDateDifference = (unitOfTime: DateUnit | TimeUnit) => - momentEnd.diff(momentStart, unitOfTime); - - if (getDateDifference('years') >= 5) { + if (getDateDifference(start, end, 'years') >= 5) { return { dateFormat: getDateFormat('years') }; } - if (getDateDifference('months') >= 5) { + if (getDateDifference(start, end, 'months') >= 5) { return { dateFormat: getDateFormat('months') }; } const dateFormatWithDays = getDateFormat('days'); - if (getDateDifference('days') > 1) { + if (getDateDifference(start, end, 'days') > 1) { return { dateFormat: dateFormatWithDays }; } - if (getDateDifference('hours') >= 5) { + if (getDateDifference(start, end, 'hours') >= 5) { return { dateFormat: dateFormatWithDays, timeFormat: getTimeFormat('minutes') }; } - if (getDateDifference('minutes') >= 5) { + if (getDateDifference(start, end, 'minutes') >= 5) { return { dateFormat: dateFormatWithDays, timeFormat: getTimeFormat('seconds') diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts index 56cee04049bd98..b5d7747c23a2c4 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -16,6 +16,7 @@ import { SetupTimeRange, SetupUIFilters } from '../../helpers/setup_request'; +import { ProcessorEvent } from '../../../../common/processor_event'; export async function getTransaction( transactionId: string, @@ -31,7 +32,7 @@ export async function getTransaction( query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [TRANSACTION_ID]: transactionId } }, { term: { [TRACE_ID]: traceId } }, { range: rangeFilter(start, end) }, diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts new file mode 100644 index 00000000000000..a753908c545c4f --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction_by_trace/index.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + PROCESSOR_EVENT, + TRACE_ID, + PARENT_ID +} from '../../../../common/elasticsearch_fieldnames'; +import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; +import { Setup } from '../../helpers/setup_request'; +import { ProcessorEvent } from '../../../../common/processor_event'; + +export async function getRootTransactionByTraceId( + traceId: string, + setup: Setup +) { + const { client, indices } = setup; + const params = { + index: indices['apm_oss.transactionIndices'], + body: { + size: 1, + query: { + bool: { + should: [ + { + constant_score: { + filter: { + bool: { + must_not: { exists: { field: PARENT_ID } } + } + } + } + } + ], + filter: [ + { term: { [TRACE_ID]: traceId } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } } + ] + } + } + } + }; + + const resp = await client.search(params); + return { + transaction: resp.hits.hits[0]?._source + }; +} diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts index 1735aa9da7dca3..95488591d4b89f 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts @@ -37,6 +37,7 @@ import { import { metricsChartsRoute } from './metrics'; import { serviceNodesRoute } from './service_nodes'; import { tracesRoute, tracesByIdRoute } from './traces'; +import { transactionByTraceIdRoute } from './transaction'; import { transactionGroupsBreakdownRoute, transactionGroupsChartsRoute, @@ -115,7 +116,10 @@ const createApmApi = () => { .add(transactionsLocalFiltersRoute) .add(serviceNodesLocalFiltersRoute) .add(uiFiltersEnvironmentsRoute) - .add(serviceMapRoute); + .add(serviceMapRoute) + + // Transaction + .add(transactionByTraceIdRoute); return api; }; diff --git a/x-pack/legacy/plugins/apm/server/routes/transaction.ts b/x-pack/legacy/plugins/apm/server/routes/transaction.ts new file mode 100644 index 00000000000000..92bf75a6af256c --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/routes/transaction.ts @@ -0,0 +1,24 @@ +/* + * 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 * as t from 'io-ts'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; +import { createRoute } from './create_route'; + +export const transactionByTraceIdRoute = createRoute(() => ({ + path: '/api/apm/transaction/{traceId}', + params: { + path: t.type({ + traceId: t.string + }) + }, + handler: async ({ context, request }) => { + const { traceId } = context.params.path; + const setup = await setupRequest(context, request); + return getRootTransactionByTraceId(traceId, setup); + } +})); From d421964b13dfa6506a3601f7c3e6ad6ec96579ed Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Wed, 4 Dec 2019 14:33:22 +0100 Subject: [PATCH 02/30] [ML] Perform cardinality check on enabling the model plot (#51915) * [ML] add a callout * [ML] reactive validateCardinality$ * [ML] change check to analysis_config * [ML] change comment * [ML] WIP check cardinality in job validator * [ML] refactor to use jobValidatorUpdated * [ML] rename vars * [ML] rename config fields * [ML] improve stream to cache and compare only analysis_config * [ML] simplify jobCreator subject * [ML] remove condition from effect * [ML] PR remarks --- .../common/job_validator/job_validator.ts | 58 +++++++++++++- .../common/job_validator/validators.ts | 76 +++++++++++++++++++ .../components/mml_callout.tsx | 44 +++++++++++ .../model_plot/model_plot_switch.tsx | 35 +++++---- .../jobs/new_job/pages/new_job/wizard.tsx | 10 +++ .../services/ml_api_service/index.d.ts | 14 ++++ .../services/ml_api_service/index.js | 7 +- 7 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts create mode 100644 x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/mml_callout.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts index 550b579c93392d..3c1f767aeaf9cb 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts @@ -5,20 +5,30 @@ */ import { ReactElement } from 'react'; +import { combineLatest, Observable, ReplaySubject, Subject } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; import { basicJobValidation, basicDatafeedValidation, } from '../../../../../../common/util/job_utils'; import { getNewJobLimits } from '../../../../services/ml_server_info'; -import { JobCreatorType } from '../job_creator'; +import { JobCreator, JobCreatorType } from '../job_creator'; import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util'; import { ExistingJobsAndGroups } from '../../../../services/job_service'; +import { cardinalityValidator, CardinalityValidatorResult } from './validators'; // delay start of validation to allow the user to make changes // e.g. if they are typing in a new value, try not to validate // after every keystroke const VALIDATION_DELAY_MS = 500; +type AsyncValidatorsResult = Partial; + +/** + * Union of possible validation results. + */ +export type JobValidationResult = BasicValidations & AsyncValidatorsResult; + export interface ValidationSummary { basic: boolean; advanced: boolean; @@ -47,6 +57,8 @@ export class JobValidator { private _lastJobConfig: string; private _lastDatafeedConfig: string; private _validateTimeout: ReturnType | null = null; + private _asyncValidators$: Array> = []; + private _asyncValidatorsResult$: Observable; private _existingJobsAndGroups: ExistingJobsAndGroups; private _basicValidations: BasicValidations = { jobId: { valid: true }, @@ -60,6 +72,14 @@ export class JobValidator { scrollSize: { valid: true }, }; private _validating: boolean = false; + private _basicValidationResult$ = new ReplaySubject(2); + + private _jobCreatorSubject$ = new Subject(); + + /** + * Observable that combines basic and async validation results. + */ + public validationResult$: Observable; constructor(jobCreator: JobCreatorType, existingJobsAndGroups: ExistingJobsAndGroups) { this._jobCreator = jobCreator; @@ -70,12 +90,43 @@ export class JobValidator { advanced: false, }; this._existingJobsAndGroups = existingJobsAndGroups; + + this._asyncValidators$ = [cardinalityValidator(this._jobCreatorSubject$)]; + + this._asyncValidatorsResult$ = combineLatest(this._asyncValidators$).pipe( + map(res => { + return res.reduce((acc, curr) => { + return { + ...acc, + ...(curr ? curr : {}), + }; + }, {}); + }) + ); + + this.validationResult$ = combineLatest([ + this._basicValidationResult$, + this._asyncValidatorsResult$, + ]).pipe( + map(([basicValidationResult, asyncValidatorsResult]) => { + return { + ...basicValidationResult, + ...asyncValidatorsResult, + }; + }), + tap(latestValidationResult => { + this.latestValidationResult = latestValidationResult; + }) + ); } + latestValidationResult: JobValidationResult = this._basicValidations; + public validate(callback: () => void, forceValidate: boolean = false) { this._validating = true; const formattedJobConfig = this._jobCreator.formattedJobJson; const formattedDatafeedConfig = this._jobCreator.formattedDatafeedJson; + // only validate if the config has changed if ( forceValidate || @@ -90,6 +141,9 @@ export class JobValidator { this._lastDatafeedConfig = formattedDatafeedConfig; this._validateTimeout = setTimeout(() => { this._runBasicValidation(); + + this._jobCreatorSubject$.next(this._jobCreator); + this._validating = false; this._validateTimeout = null; callback(); @@ -137,6 +191,8 @@ export class JobValidator { populateValidationMessages(idResults, this._basicValidations, jobConfig, datafeedConfig); this._validationSummary.basic = this._isOverallBasicValid(); + // Update validation results subject + this._basicValidationResult$.next(this._basicValidations); } private _isOverallBasicValid() { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts new file mode 100644 index 00000000000000..85018ab2f7944e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts @@ -0,0 +1,76 @@ +/* + * 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 { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; +import { Observable, Subject } from 'rxjs'; +import { + CardinalityModelPlotHigh, + CardinalityValidationResult, + ml, +} from '../../../../services/ml_api_service'; +import { JobCreator } from '../job_creator'; + +export enum VALIDATOR_SEVERITY { + ERROR, + WARNING, +} + +export interface CardinalityValidatorError { + highCardinality: { + value: number; + severity: VALIDATOR_SEVERITY; + }; +} + +export type CardinalityValidatorResult = CardinalityValidatorError | null; + +export function isCardinalityModelPlotHigh( + cardinalityValidationResult: CardinalityValidationResult +): cardinalityValidationResult is CardinalityModelPlotHigh { + return ( + (cardinalityValidationResult as CardinalityModelPlotHigh).modelPlotCardinality !== undefined + ); +} + +export function cardinalityValidator( + jobCreator$: Subject +): Observable { + return jobCreator$.pipe( + // Perform a cardinality check only with enabled model plot. + filter(jobCreator => { + return jobCreator?.modelPlot; + }), + map(jobCreator => { + return { + jobCreator, + analysisConfigString: JSON.stringify(jobCreator.jobConfig.analysis_config), + }; + }), + // No need to perform an API call if the analysis configuration hasn't been changed + distinctUntilChanged((prev, curr) => { + return prev.analysisConfigString === curr.analysisConfigString; + }), + switchMap(({ jobCreator }) => { + return ml.validateCardinality$({ + ...jobCreator.jobConfig, + datafeed_config: jobCreator.datafeedConfig, + }); + }), + map(validationResults => { + for (const validationResult of validationResults) { + if (isCardinalityModelPlotHigh(validationResult)) { + return { + highCardinality: { + value: validationResult.modelPlotCardinality, + severity: VALIDATOR_SEVERITY.WARNING, + }, + }; + } + } + return null; + }) + ); +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/mml_callout.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/mml_callout.tsx new file mode 100644 index 00000000000000..754b8e86e3bd33 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/mml_callout.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useContext, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCallOut, EuiText } from '@elastic/eui'; +import { JobCreatorContext } from '../../../../job_creator_context'; + +export const MMLCallout: FC = () => { + const { jobCreator, jobValidator, jobValidatorUpdated } = useContext(JobCreatorContext); + const [highCardinality, setHighCardinality] = useState(null); + + useEffect(() => { + const value = jobValidator.latestValidationResult?.highCardinality?.value ?? null; + setHighCardinality(value); + }, [jobValidatorUpdated]); + + return jobCreator.modelPlot && highCardinality !== null ? ( + + } + color="warning" + iconType="help" + > + + + + + ) : null; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx index 226742bf3e97a4..64f9f450ae08d2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/components/model_plot/model_plot_switch.tsx @@ -6,9 +6,10 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSwitch } from '@elastic/eui'; +import { EuiSpacer, EuiSwitch } from '@elastic/eui'; import { JobCreatorContext } from '../../../../../job_creator_context'; import { Description } from './description'; +import { MMLCallout } from '../mml_callout'; export const ModelPlotSwitch: FC = () => { const { jobCreator, jobCreatorUpdate } = useContext(JobCreatorContext); @@ -24,19 +25,23 @@ export const ModelPlotSwitch: FC = () => { } return ( - - - + <> + + + + + + ); }; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx index 890db5e29387ad..50b8650f99bb89 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx @@ -76,6 +76,16 @@ export const Wizard: FC = ({ stringifyConfigs(jobCreator.jobConfig, jobCreator.datafeedConfig) ); + useEffect(() => { + const subscription = jobValidator.validationResult$.subscribe(() => { + setJobValidatorUpdate(jobValidatorUpdated); + }); + + return () => { + return subscription.unsubscribe(); + }; + }, []); + useEffect(() => { jobValidator.validate(() => { setJobValidatorUpdate(jobValidatorUpdated); diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts index a21bce76ddc1f1..ad600ad2cbd710 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.d.ts @@ -18,6 +18,7 @@ import { DataFrameAnalyticsConfig } from '../../data_frame_analytics/common/anal import { DeepPartial } from '../../../../common/types/common'; import { annotations } from './annotations'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; +import { CombinedJob } from '../../jobs/new_job/common/job_creator/configs'; // TODO This is not a complete representation of all methods of `ml.*`. // It just satisfies needs for other parts of the code area which use @@ -64,6 +65,18 @@ export interface MlInfoResponse { cloudId?: string; } +export interface SuccessCardinality { + id: 'success_cardinality'; +} + +export interface CardinalityModelPlotHigh { + id: 'cardinality_model_plot_high'; + modelPlotCardinality: number; +} + +export type CardinalityValidationResult = SuccessCardinality | CardinalityModelPlotHigh; +export type CardinalityValidationResults = CardinalityValidationResult[]; + declare interface Ml { annotations: { deleteAnnotation(id: string | undefined): Promise; @@ -159,6 +172,7 @@ declare interface Ml { mlNodeCount(): Promise<{ count: number }>; mlInfo(): Promise; getCardinalityOfFields(obj: Record): any; + validateCardinality$(job: CombinedJob): Observable; } declare const ml: Ml; diff --git a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js index fadb05efd12cad..29b112dc8f456d 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js +++ b/x-pack/legacy/plugins/ml/public/application/services/ml_api_service/index.js @@ -95,11 +95,10 @@ export const ml = { }); }, - validateCardinality(obj) { - return http({ - url: `${basePath}/validate/cardinality`, + validateCardinality$(obj) { + return http$(`${basePath}/validate/cardinality`, { method: 'POST', - data: obj + body: obj }); }, From 529dcbbe30736e63fd05bf0e92e06cd7aa631da7 Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Wed, 4 Dec 2019 08:36:07 -0500 Subject: [PATCH 03/30] [Lens] Remove unused datasource methods (#51840) * Remove unused datasource methods * Remove unused function --- .../lens/public/editor_frame_plugin/mocks.tsx | 3 - .../indexpattern_plugin/indexpattern.test.ts | 55 ------------------- .../indexpattern_plugin/indexpattern.tsx | 22 -------- x-pack/legacy/plugins/lens/public/types.ts | 4 -- 4 files changed, 84 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 79dff5bddb8823..456951e7d2d260 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -48,9 +48,6 @@ export function createMockDatasource(): DatasourceMock { getOperationForColumnId: jest.fn(), renderDimensionPanel: jest.fn(), renderLayerPanel: jest.fn(), - removeColumnInTableSpec: jest.fn(), - moveColumnTo: jest.fn(), - duplicateColumn: jest.fn(), }; return { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts index d998437f3e4920..e7def3b9dbf2c0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts @@ -433,61 +433,6 @@ describe('IndexPattern Data Source', () => { }); }); - describe('removeColumnInTableSpec', () => { - it('should remove the specified column', async () => { - const initialState = await indexPatternDatasource.initialize(persistedState); - const setState = jest.fn(); - const sampleColumn: IndexPatternColumn = { - dataType: 'number', - isBucketed: false, - label: 'foo', - operationType: 'max', - sourceField: 'baz', - suggestedPriority: 0, - }; - const columns: Record = { - a: { - ...sampleColumn, - suggestedPriority: 0, - }, - b: { - ...sampleColumn, - suggestedPriority: 1, - }, - c: { - ...sampleColumn, - suggestedPriority: 2, - }, - }; - const api = indexPatternDatasource.getPublicAPI({ - state: { - ...initialState, - layers: { - first: { - ...initialState.layers.first, - columns, - columnOrder: ['a', 'b', 'c'], - }, - }, - }, - setState, - layerId: 'first', - dateRange: { - fromDate: 'now-1y', - toDate: 'now', - }, - }); - - api.removeColumnInTableSpec('b'); - - expect(setState.mock.calls[0][0].layers.first.columnOrder).toEqual(['a', 'c']); - expect(setState.mock.calls[0][0].layers.first.columns).toEqual({ - a: columns.a, - c: columns.c, - }); - }); - }); - describe('getOperationForColumnId', () => { it('should get an operation for col1', () => { expect(publicAPI.getOperationForColumnId('col1')).toEqual({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 443667ab644767..7a413f24f243a1 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -89,12 +89,6 @@ export function uniqueLabels(layers: Record) { return columnLabelMap; } -function removeProperty(prop: string, object: Record): Record { - const result = { ...object }; - delete result[prop]; - return result; -} - export function getIndexPatternDatasource({ chrome, core, @@ -268,22 +262,6 @@ export function getIndexPatternDatasource({ domElement ); }, - - removeColumnInTableSpec: (columnId: string) => { - setState({ - ...state, - layers: { - ...state.layers, - [layerId]: { - ...state.layers[layerId], - columnOrder: state.layers[layerId].columnOrder.filter(id => id !== columnId), - columns: removeProperty(columnId, state.layers[layerId].columns), - }, - }, - }); - }, - moveColumnTo: () => {}, - duplicateColumn: () => [], }; }, getDatasourceSuggestionsForField(state, draggedField) { diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 36d30fa1a6d485..8c1e90643ca7af 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -157,10 +157,6 @@ export interface DatasourcePublicAPI { // Render can be called many times renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; - - removeColumnInTableSpec: (columnId: string) => void; - moveColumnTo: (columnId: string, targetIndex: number) => void; - duplicateColumn: (columnId: string) => TableSpec; } export interface TableSpecColumn { From 8cae172f0246307b520500a28a07a33ab529f8ef Mon Sep 17 00:00:00 2001 From: Chris Davies Date: Wed, 4 Dec 2019 08:36:21 -0500 Subject: [PATCH 04/30] [Lens] Make Lens plugin registry signatures consistent (#51839) --- x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx | 2 +- .../legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx | 1 + .../legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx | 4 ++-- .../plugins/lens/public/indexpattern_plugin/indexpattern.tsx | 2 ++ x-pack/legacy/plugins/lens/public/types.ts | 4 +++- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index f2678463f57da0..6493af28f89ea9 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -73,7 +73,7 @@ export class AppPlugin { editorFrameSetupInterface.registerVisualization(xyVisualization); editorFrameSetupInterface.registerVisualization(datatableVisualization); editorFrameSetupInterface.registerVisualization(metricVisualization); - editorFrameSetupInterface.registerDatasource('indexpattern', indexPattern); + editorFrameSetupInterface.registerDatasource(indexPattern); kibana_legacy.registerLegacyApp({ id: 'lens', diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 456951e7d2d260..5df6cc8106d6af 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -51,6 +51,7 @@ export function createMockDatasource(): DatasourceMock { }; return { + id: 'mockindexpattern', getDatasourceSuggestionsForField: jest.fn((_state, item) => []), getDatasourceSuggestionsFromCurrentState: jest.fn(_state => []), getPersistableState: jest.fn(), diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index f7399255b20018..5c22111cd1caec 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -57,8 +57,8 @@ export class EditorFramePlugin { plugins.expressions.registerFunction(() => mergeTables); return { - registerDatasource: (name, datasource) => { - this.datasources[name] = datasource as Datasource; + registerDatasource: datasource => { + this.datasources[datasource.id] = datasource as Datasource; }, registerVisualization: visualization => { this.visualizations[visualization.id] = visualization as Visualization; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 7a413f24f243a1..15f19bb9d97e6b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -113,6 +113,8 @@ export function getIndexPatternDatasource({ // Not stateful. State is persisted to the frame const indexPatternDatasource: Datasource = { + id: 'indexpattern', + initialize(state?: IndexPatternPersistedState) { return loadInitialState({ state, savedObjectsClient }); }, diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 8c1e90643ca7af..f83157b2a80000 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -47,7 +47,7 @@ export interface EditorFrameInstance { export interface EditorFrameSetup { // generic type on the API functions to pull the "unknown vs. specific type" error into the implementation - registerDatasource: (name: string, datasource: Datasource) => void; + registerDatasource: (datasource: Datasource) => void; registerVisualization: (visualization: Visualization) => void; } @@ -123,6 +123,8 @@ export type StateSetter = (newState: T | ((prevState: T) => T)) => void; * Interface for the datasource registry */ export interface Datasource { + id: string; + // For initializing, either from an empty state or from persisted state // Because this will be called at runtime, state might have a type of `any` and // datasources should validate their arguments From f188c261112fd3be34c21cdd62385ea4c35f2519 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 4 Dec 2019 08:49:10 -0500 Subject: [PATCH 05/30] [SR] Support for SLM on Cloud (#51000) --- .../common/lib/policy_serialization.test.ts | 107 +++-- .../common/lib/policy_serialization.ts | 7 +- .../common/lib/snapshot_serialization.test.ts | 114 +++-- .../common/lib/snapshot_serialization.ts | 9 +- .../snapshot_restore/common/types/policy.ts | 1 + .../snapshot_restore/common/types/snapshot.ts | 3 +- .../components/policy_form/policy_form.tsx | 11 +- .../app/components/policy_form/steps/index.ts | 2 +- .../policy_form/steps/step_logistics.tsx | 96 +++- .../policy_form/steps/step_settings.tsx | 428 +++++++++--------- .../policy_details/policy_details.tsx | 1 + .../policy_details/tabs/tab_summary.tsx | 18 + .../policy_list/policy_table/policy_table.tsx | 48 +- .../home/repository_list/repository_list.tsx | 6 +- .../repository_table/repository_table.tsx | 17 +- .../snapshot_details/snapshot_details.tsx | 10 +- .../snapshot_table/snapshot_table.tsx | 47 +- .../app/sections/policy_add/policy_add.tsx | 1 + .../app/sections/policy_edit/policy_edit.tsx | 43 +- .../services/validation/validate_policy.ts | 29 +- .../server/lib/get_managed_policy_names.ts | 30 ++ .../snapshot_restore/server/lib/index.ts | 1 + .../server/routes/api/policy.test.ts | 1 + .../server/routes/api/policy.ts | 13 +- .../server/routes/api/repositories.test.ts | 43 +- .../server/routes/api/repositories.ts | 45 +- .../server/routes/api/snapshots.test.ts | 6 +- .../server/routes/api/snapshots.ts | 34 +- .../snapshot_restore/test/fixtures/policy.ts | 2 + 29 files changed, 786 insertions(+), 387 deletions(-) create mode 100644 x-pack/legacy/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts index 9ce9367bc0e0ea..81cf705124e78a 100644 --- a/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.test.ts @@ -9,31 +9,35 @@ describe('repository_serialization', () => { describe('deserializePolicy()', () => { it('should deserialize a new slm policy', () => { expect( - deserializePolicy('my-backups-snapshots', { - version: 1, - modified_date: '2019-07-09T22:11:55.761Z', - modified_date_millis: 1562710315761, - policy: { - name: '', - schedule: '0 30 1 * * ?', - repository: 'my-backups', - config: { - indices: ['kibana-*'], - ignore_unavailable: false, - include_global_state: false, - metadata: { - foo: 'bar', + deserializePolicy( + 'my-backups-snapshots', + { + version: 1, + modified_date: '2019-07-09T22:11:55.761Z', + modified_date_millis: 1562710315761, + policy: { + name: '', + schedule: '0 30 1 * * ?', + repository: 'my-backups', + config: { + indices: ['kibana-*'], + ignore_unavailable: false, + include_global_state: false, + metadata: { + foo: 'bar', + }, + }, + retention: { + expire_after: '14d', + max_count: 30, + min_count: 4, }, }, - retention: { - expire_after: '14d', - max_count: 30, - min_count: 4, - }, + next_execution: '2019-07-11T01:30:00.000Z', + next_execution_millis: 1562722200000, }, - next_execution: '2019-07-11T01:30:00.000Z', - next_execution_millis: 1562722200000, - }) + ['my-backups-snapshots'] + ) ).toEqual({ name: 'my-backups-snapshots', version: 1, @@ -58,41 +62,46 @@ describe('repository_serialization', () => { }, nextExecution: '2019-07-11T01:30:00.000Z', nextExecutionMillis: 1562722200000, + isManagedPolicy: true, }); }); it('should deserialize a slm policy with success and failure info', () => { expect( - deserializePolicy('my-backups-snapshots', { - version: 1, - modified_date: '2019-07-09T22:11:55.761Z', - modified_date_millis: 1562710315761, - policy: { - name: '', - schedule: '0 30 1 * * ?', - repository: 'my-backups', - config: { - indices: ['kibana-*'], - ignore_unavailable: false, - include_global_state: false, + deserializePolicy( + 'my-backups-snapshots', + { + version: 1, + modified_date: '2019-07-09T22:11:55.761Z', + modified_date_millis: 1562710315761, + policy: { + name: '', + schedule: '0 30 1 * * ?', + repository: 'my-backups', + config: { + indices: ['kibana-*'], + ignore_unavailable: false, + include_global_state: false, + }, }, - }, - next_execution: '2019-07-11T01:30:00.000Z', - next_execution_millis: 1562722200000, - last_success: { - snapshot_name: 'daily-snap-2019.07.10-ya_cajvksbcidtlbnnxt9q', - time_string: '2019-07-10T01:30:02.548Z', - time: 1562722202548, - }, - last_failure: { - snapshot_name: 'daily-snap-2019.07.10-cvi4m0uts5knejcrgq4qxq', - time_string: '2019-07-10T01:30:02.443Z', - time: 1562722202443, - details: `{"type":"concurrent_snapshot_execution_exception", + next_execution: '2019-07-11T01:30:00.000Z', + next_execution_millis: 1562722200000, + last_success: { + snapshot_name: 'daily-snap-2019.07.10-ya_cajvksbcidtlbnnxt9q', + time_string: '2019-07-10T01:30:02.548Z', + time: 1562722202548, + }, + last_failure: { + snapshot_name: 'daily-snap-2019.07.10-cvi4m0uts5knejcrgq4qxq', + time_string: '2019-07-10T01:30:02.443Z', + time: 1562722202443, + details: `{"type":"concurrent_snapshot_execution_exception", "reason":"[my-backups:daily-snap-2019.07.10-cvi4m0uts5knejcrgq4qxq] a snapshot is already running", "stack_trace":"Some stack trace"}`, + }, }, - }) + ['my-backups-snapshots'] + ) ).toEqual({ name: 'my-backups-snapshots', version: 1, @@ -120,6 +129,7 @@ describe('repository_serialization', () => { time: 1562722202548, timeString: '2019-07-10T01:30:02.548Z', }, + isManagedPolicy: true, }); }); }); @@ -146,6 +156,7 @@ describe('repository_serialization', () => { maxCount: 30, minCount: 4, }, + isManagedPolicy: true, }) ).toEqual({ name: 'my-backups-snapshots', diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts index 4652ac4bc5cc45..6baf9794995f21 100644 --- a/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts +++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/policy_serialization.ts @@ -11,7 +11,11 @@ import { serializeSnapshotRetention, } from './'; -export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolicy => { +export const deserializePolicy = ( + name: string, + esPolicy: SlmPolicyEs, + managedPolicies: string[] +): SlmPolicy => { const { version, modified_date: modifiedDate, @@ -35,6 +39,7 @@ export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolic repository, nextExecution, nextExecutionMillis, + isManagedPolicy: managedPolicies.includes(name), }; if (config) { diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts index 1900580f6c43aa..298fc235fd9cc7 100644 --- a/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.test.ts @@ -9,43 +9,86 @@ import { deserializeSnapshotDetails } from './snapshot_serialization'; describe('deserializeSnapshotDetails', () => { test('deserializes a snapshot', () => { expect( - deserializeSnapshotDetails('repositoryName', { - snapshot: 'snapshot name', - uuid: 'UUID', - version_id: 5, - version: 'version', - indices: ['index2', 'index3', 'index1'], - include_global_state: false, - state: 'SUCCESS', - start_time: '0', - start_time_in_millis: 0, - end_time: '1', - end_time_in_millis: 1, - duration_in_millis: 1, - shards: { - total: 3, - failed: 1, - successful: 2, - }, - failures: [ - { - index: 'z', - shard: 1, - }, - { - index: 'a', - shard: 3, - }, - { - index: 'a', - shard: 1, + deserializeSnapshotDetails( + 'repositoryName', + { + snapshot: 'snapshot name', + uuid: 'UUID', + version_id: 5, + version: 'version', + indices: ['index2', 'index3', 'index1'], + include_global_state: false, + state: 'SUCCESS', + start_time: '0', + start_time_in_millis: 0, + end_time: '1', + end_time_in_millis: 1, + duration_in_millis: 1, + shards: { + total: 3, + failed: 1, + successful: 2, }, + failures: [ + { + index: 'z', + shard: 1, + }, + { + index: 'a', + shard: 3, + }, + { + index: 'a', + shard: 1, + }, + { + index: 'a', + shard: 2, + }, + ], + }, + 'found-snapshots', + [ { - index: 'a', - shard: 2, + snapshot: 'last_successful_snapshot', + uuid: 'last_successful_snapshot_UUID', + version_id: 5, + version: 'version', + indices: ['index2', 'index3', 'index1'], + include_global_state: false, + state: 'SUCCESS', + start_time: '0', + start_time_in_millis: 0, + end_time: '1', + end_time_in_millis: 1, + duration_in_millis: 1, + shards: { + total: 3, + failed: 1, + successful: 2, + }, + failures: [ + { + index: 'z', + shard: 1, + }, + { + index: 'a', + shard: 3, + }, + { + index: 'a', + shard: 1, + }, + { + index: 'a', + shard: 2, + }, + ], }, - ], - }) + ] + ) ).toEqual({ repository: 'repositoryName', snapshot: 'snapshot name', @@ -91,7 +134,8 @@ describe('deserializeSnapshotDetails', () => { failed: 1, successful: 2, }, - isManagedRepository: false, + managedRepository: 'found-snapshots', + isLastSuccessfulSnapshot: false, }); }); }); diff --git a/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts b/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts index 50fdef4175787c..1bfe5e49fa09dc 100644 --- a/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts +++ b/x-pack/legacy/plugins/snapshot_restore/common/lib/snapshot_serialization.ts @@ -20,7 +20,8 @@ import { deserializeTime, serializeTime } from './time_serialization'; export function deserializeSnapshotDetails( repository: string, snapshotDetailsEs: SnapshotDetailsEs, - managedRepository?: string + managedRepository?: string, + successfulSnapshots?: SnapshotDetailsEs[] ): SnapshotDetails { if (!snapshotDetailsEs || typeof snapshotDetailsEs !== 'object') { throw new Error('Unable to deserialize snapshot details'); @@ -85,9 +86,13 @@ export function deserializeSnapshotDetails( durationInMillis, indexFailures, shards, - isManagedRepository: repository === managedRepository, + managedRepository, }; + if (successfulSnapshots && successfulSnapshots.length) { + snapshotDetails.isLastSuccessfulSnapshot = successfulSnapshots[0].snapshot === snapshot; + } + if (policyName) { snapshotDetails.policyName = policyName; } diff --git a/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts b/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts index ed67b1eb77063d..33ce10c84bb27c 100644 --- a/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/common/types/policy.ts @@ -17,6 +17,7 @@ export interface SlmPolicyPayload { repository: string; config?: SnapshotConfig; retention?: SnapshotRetention; + isManagedPolicy: boolean; } export interface SlmPolicy extends SlmPolicyPayload { diff --git a/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts b/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts index 46713c937fd3f0..a46f5c7921bfe0 100644 --- a/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts +++ b/x-pack/legacy/plugins/snapshot_restore/common/types/snapshot.ts @@ -41,8 +41,9 @@ export interface SnapshotDetails { durationInMillis: number; indexFailures: any[]; shards: SnapshotDetailsShardsStatus; - isManagedRepository?: boolean; + managedRepository?: string; policyName?: string; + isLastSuccessfulSnapshot?: boolean; } export interface SnapshotDetailsEs { diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx index 7e55cee63a0ac4..72e3ec05facfab 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/policy_form.tsx @@ -83,9 +83,12 @@ export const PolicyForm: React.FunctionComponent = ({ errors: {}, }); - const updatePolicy = (updatedFields: any): void => { + const updatePolicy = ( + updatedFields: Partial, + validationHelperData = {} + ): void => { const newPolicy = { ...policy, ...updatedFields }; - const newValidation = validatePolicy(newPolicy); + const newValidation = validatePolicy(newPolicy, validationHelperData); setPolicy(newPolicy); setValidation(newValidation); }; @@ -187,8 +190,8 @@ export const PolicyForm: React.FunctionComponent = ({ {currentStep === lastStep ? ( savePolicy()} isLoading={isSaving} diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/index.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/index.ts index 839df50f79a6fa..8b251de80a8e1e 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/index.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/index.ts @@ -10,7 +10,7 @@ import { PolicyValidation } from '../../../services/validation'; export interface StepProps { policy: SlmPolicyPayload; indices: string[]; - updatePolicy: (updatedSettings: Partial) => void; + updatePolicy: (updatedSettings: Partial, validationHelperData?: any) => void; isEditing: boolean; currentUrl: string; errors: PolicyValidation['errors']; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_logistics.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_logistics.tsx index 7c3036b3d79c29..2206d6de341c84 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_logistics.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_logistics.tsx @@ -46,8 +46,11 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ const { error: errorLoadingRepositories, isLoading: isLoadingRepositories, - data: { repositories } = { + data: { repositories, managedRepository } = { repositories: [], + managedRepository: { + name: undefined, + }, }, sendRequest: reloadRepositories, } = useLoadRepositories(); @@ -111,9 +114,16 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ fullWidth onBlur={() => setTouched({ ...touched, name: true })} onChange={e => { - updatePolicy({ - name: e.target.value, - }); + updatePolicy( + { + name: e.target.value, + }, + { + managedRepository, + isEditing, + policyName: policy.name, + } + ); }} placeholder={i18n.translate( 'xpack.snapshotRestore.policyForm.stepLogistics.namePlaceholder', @@ -240,9 +250,16 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ ); } else { if (!policy.repository) { - updatePolicy({ - repository: repositories[0].name, - }); + updatePolicy( + { + repository: repositories[0].name, + }, + { + managedRepository, + isEditing, + policyName: policy.name, + } + ); } } @@ -255,9 +272,16 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ value={policy.repository || repositories[0].name} onBlur={() => setTouched({ ...touched, repository: true })} onChange={e => { - updatePolicy({ - repository: e.target.value, - }); + updatePolicy( + { + repository: e.target.value, + }, + { + managedRepository, + isEditing, + policyName: policy.name, + } + ); }} fullWidth data-test-subj="repositorySelect" @@ -321,9 +345,16 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ defaultValue={policy.snapshotName} fullWidth onChange={e => { - updatePolicy({ - snapshotName: e.target.value.toLowerCase(), - }); + updatePolicy( + { + snapshotName: e.target.value.toLowerCase(), + }, + { + managedRepository, + isEditing, + policyName: policy.name, + } + ); }} onBlur={() => setTouched({ ...touched, snapshotName: true })} placeholder={i18n.translate( @@ -395,9 +426,16 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ defaultValue={policy.schedule} fullWidth onChange={e => { - updatePolicy({ - schedule: e.target.value, - }); + updatePolicy( + { + schedule: e.target.value, + }, + { + managedRepository, + isEditing, + policyName: policy.name, + } + ); }} onBlur={() => setTouched({ ...touched, schedule: true })} placeholder={DEFAULT_POLICY_SCHEDULE} @@ -411,9 +449,16 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ { setIsAdvancedCronVisible(false); - updatePolicy({ - schedule: simpleCron.expression, - }); + updatePolicy( + { + schedule: simpleCron.expression, + }, + { + managedRepository, + isEditing, + policyName: policy.name, + } + ); }} data-test-subj="showBasicCronLink" > @@ -444,9 +489,16 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ frequency, }); setFieldToPreferredValueMap(newFieldToPreferredValueMap); - updatePolicy({ - schedule: expression, - }); + updatePolicy( + { + schedule: expression, + }, + { + managedRepository, + isEditing, + policyName: policy.name, + } + ); }} /> diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx index 0997d59b2216d3..0e3b6e030d1c61 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/components/policy_form/steps/step_settings.tsx @@ -18,6 +18,7 @@ import { EuiSelectable, EuiPanel, EuiComboBox, + EuiToolTip, } from '@elastic/eui'; import { Option } from '@elastic/eui/src/components/selectable/types'; import { SlmPolicyPayload, SnapshotConfig } from '../../../../../common/types'; @@ -35,7 +36,7 @@ export const PolicyStepSettings: React.FunctionComponent = ({ core: { i18n }, } = useAppDependencies(); const { FormattedMessage } = i18n; - const { config = {} } = policy; + const { config = {}, isManagedPolicy } = policy; const updatePolicyConfig = (updatedFields: Partial): void => { const newConfig = { ...config, ...updatedFields }; @@ -78,216 +79,239 @@ export const PolicyStepSettings: React.FunctionComponent = ({ typeof config.indices === 'string' ? config.indices.split(',') : [] ); - const renderIndicesField = () => ( - -

- -

- - } - description={ - - } - idAria="indicesDescription" - fullWidth - > - - - { + const indicesSwitch = ( + + } + checked={isAllIndices} + disabled={isManagedPolicy} + data-test-subj="allIndicesToggle" + onChange={e => { + const isChecked = e.target.checked; + setIsAllIndices(isChecked); + if (isChecked) { + updatePolicyConfig({ indices: undefined }); + } else { + updatePolicyConfig({ + indices: + selectIndicesMode === 'custom' + ? indexPatterns.join(',') + : [...(indicesSelection || [])], + }); + } + }} + /> + ); + + return ( + +

- } - checked={isAllIndices} - data-test-subj="allIndicesToggle" - onChange={e => { - const isChecked = e.target.checked; - setIsAllIndices(isChecked); - if (isChecked) { - updatePolicyConfig({ indices: undefined }); - } else { - updatePolicyConfig({ - indices: - selectIndicesMode === 'custom' - ? indexPatterns.join(',') - : [...(indicesSelection || [])], - }); - } - }} +

+ + } + description={ + - {isAllIndices ? null : ( - - - - - - - - { - setSelectIndicesMode('custom'); - updatePolicyConfig({ indices: indexPatterns.join(',') }); - }} - > + } + idAria="indicesDescription" + fullWidth + > + + + {isManagedPolicy ? ( + + +

+ } + > + {indicesSwitch} +
+ ) : ( + indicesSwitch + )} + {isAllIndices ? null : ( + + + + -
-
- - ) : ( - - - - - - { - setSelectIndicesMode('list'); - updatePolicyConfig({ indices: indicesSelection }); - }} - > + + + { + setSelectIndicesMode('custom'); + updatePolicyConfig({ indices: indexPatterns.join(',') }); + }} + > + + + + + ) : ( + + -
-
- - ) - } - helpText={ - selectIndicesMode === 'list' ? ( - 0 ? ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: Option) => { - option.checked = undefined; - }); - updatePolicyConfig({ indices: [] }); - setIndicesSelection([]); - }} - > - - - ) : ( - { - // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed - indicesOptions.forEach((option: Option) => { - option.checked = 'on'; - }); - updatePolicyConfig({ indices: [...indices] }); - setIndicesSelection([...indices]); - }} - > - - - ), + + + { + setSelectIndicesMode('list'); + updatePolicyConfig({ indices: indicesSelection }); + }} + > + + + + + ) + } + helpText={ + selectIndicesMode === 'list' ? ( + 0 ? ( + { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesOptions.forEach((option: Option) => { + option.checked = undefined; + }); + updatePolicyConfig({ indices: [] }); + setIndicesSelection([]); + }} + > + + + ) : ( + { + // TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed + indicesOptions.forEach((option: Option) => { + option.checked = 'on'; + }); + updatePolicyConfig({ indices: [...indices] }); + setIndicesSelection([...indices]); + }} + > + + + ), + }} + /> + ) : null + } + isInvalid={Boolean(errors.indices)} + error={errors.indices} + > + {selectIndicesMode === 'list' ? ( + { + const newSelectedIndices: string[] = []; + options.forEach(({ label, checked }) => { + if (checked === 'on') { + newSelectedIndices.push(label); + } + }); + setIndicesOptions(options); + updatePolicyConfig({ indices: newSelectedIndices }); + setIndicesSelection(newSelectedIndices); }} - /> - ) : null - } - isInvalid={Boolean(errors.indices)} - error={errors.indices} - > - {selectIndicesMode === 'list' ? ( - { - const newSelectedIndices: string[] = []; - options.forEach(({ label, checked }) => { - if (checked === 'on') { - newSelectedIndices.push(label); + searchable + height={300} + > + {(list, search) => ( + + {search} + {list} + + )} + + ) : ( + ({ label: index }))} + placeholder={i18n.translate( + 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder', + { + defaultMessage: 'Enter index patterns, i.e. logstash-*', } - }); - setIndicesOptions(options); - updatePolicyConfig({ indices: newSelectedIndices }); - setIndicesSelection(newSelectedIndices); - }} - searchable - height={300} - > - {(list, search) => ( - - {search} - {list} - - )} - - ) : ( - ({ label: index }))} - placeholder={i18n.translate( - 'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder', - { - defaultMessage: 'Enter index patterns, i.e. logstash-*', - } - )} - selectedOptions={indexPatterns.map(pattern => ({ label: pattern }))} - onCreateOption={(pattern: string) => { - if (!pattern.trim().length) { - return; - } - const newPatterns = [...indexPatterns, pattern]; - setIndexPatterns(newPatterns); - updatePolicyConfig({ - indices: newPatterns.join(','), - }); - }} - onChange={(patterns: Array<{ label: string }>) => { - const newPatterns = patterns.map(({ label }) => label); - setIndexPatterns(newPatterns); - updatePolicyConfig({ - indices: newPatterns.join(','), - }); - }} - /> - )} - - - )} - - - - ); + )} + selectedOptions={indexPatterns.map(pattern => ({ label: pattern }))} + onCreateOption={(pattern: string) => { + if (!pattern.trim().length) { + return; + } + const newPatterns = [...indexPatterns, pattern]; + setIndexPatterns(newPatterns); + updatePolicyConfig({ + indices: newPatterns.join(','), + }); + }} + onChange={(patterns: Array<{ label: string }>) => { + const newPatterns = patterns.map(({ label }) => label); + setIndexPatterns(newPatterns); + updatePolicyConfig({ + indices: newPatterns.join(','), + }); + }} + /> + )} + + + )} + + + + ); + }; const renderIgnoreUnavailableField = () => ( = ({ } ), icon: 'trash', + disabled: policyDetails.policy.isManagedPolicy, onClick: () => deletePolicyPrompt([policyName], onPolicyDeleted), }, diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx index 31553e110b2159..e719f3cd1451b1 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_details/tabs/tab_summary.tsx @@ -5,6 +5,7 @@ */ import React, { useState, useEffect, Fragment } from 'react'; import { + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink, @@ -46,6 +47,7 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => { config, stats, retention, + isManagedPolicy, } = policy; const { includeGlobalState, ignoreUnavailable, indices, partial } = config || { includeGlobalState: undefined, @@ -130,6 +132,22 @@ export const TabSummary: React.FunctionComponent = ({ policy }) => { return ( + {isManagedPolicy ? ( + <> + + } + /> + + + ) : null} {/** Stats panel */} {stats && ( diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx index 62038f99638366..e08753c55bf7b4 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/policy_list/policy_table/policy_table.tsx @@ -16,6 +16,7 @@ import { EuiLoadingSpinner, EuiText, EuiIcon, + EuiIconTip, } from '@elastic/eui'; import { SlmPolicy } from '../../../../../../common/types'; @@ -60,7 +61,7 @@ export const PolicyTable: React.FunctionComponent = ({ }), truncateText: true, sortable: true, - render: (name: SlmPolicy['name'], { inProgress }: SlmPolicy) => { + render: (name: SlmPolicy['name'], { inProgress, isManagedPolicy }: SlmPolicy) => { return ( @@ -71,8 +72,21 @@ export const PolicyTable: React.FunctionComponent = ({ data-test-subj="policyLink" > {name} - + {' '} + {isManagedPolicy ? ( + + + } + position="right" + /> + + ) : null} {inProgress ? ( = ({ }), actions: [ { - render: ({ name, inProgress }: SlmPolicy) => { + render: ({ name, inProgress, isManagedPolicy }: SlmPolicy) => { return ( @@ -246,13 +260,19 @@ export const PolicyTable: React.FunctionComponent = ({ {deletePolicyPrompt => { - return ( - + ) + : i18n.translate( + 'xpack.snapshotRestore.policyList.table.deleteManagedPolicyTableActionTooltip', + { + defaultMessage: 'You cannot delete a managed policy.', + } + ); + return ( + = ({ color="danger" data-test-subj="deletePolicyButton" onClick={() => deletePolicyPrompt([name], onPolicyDeleted)} + isDisabled={isManagedPolicy} /> ); @@ -294,6 +315,17 @@ export const PolicyTable: React.FunctionComponent = ({ const selection = { onSelectionChange: (newSelectedItems: SlmPolicy[]) => setSelectedItems(newSelectedItems), + selectable: ({ isManagedPolicy }: SlmPolicy) => !isManagedPolicy, + selectableMessage: (selectable: boolean) => { + if (!selectable) { + return i18n.translate( + 'xpack.snapshotRestore.policyList.table.deleteManagedPolicySelectTooltip', + { + defaultMessage: 'You cannot delete a managed policy.', + } + ); + } + }, }; const search = { diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx index 5e3250f082fce3..dbbc0e09111ebb 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/repository_list/repository_list.tsx @@ -40,7 +40,9 @@ export const RepositoryList: React.FunctionComponent = ({    {managedRepository === name ? ( - - - + + } + position="right" + /> ) : null} ); diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx index 955cade353d2ed..dd453a062fb59e 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_details/snapshot_details.tsx @@ -200,14 +200,18 @@ export const SnapshotDetails: React.FunctionComponent = ({ onSnapshotDeleted ) } - isDisabled={snapshotDetails.isManagedRepository} + isDisabled={ + snapshotDetails.managedRepository && + snapshotDetails.isLastSuccessfulSnapshot + } title={ - snapshotDetails.isManagedRepository + snapshotDetails.managedRepository && + snapshotDetails.isLastSuccessfulSnapshot ? i18n.translate( 'xpack.snapshotRestore.snapshotDetails.deleteManagedRepositorySnapshotButtonTitle', { defaultMessage: - 'You cannot delete a snapshot stored in a managed repository.', + 'You cannot delete the last successful snapshot stored in a managed repository.', } ) : null diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx index dbf584acd54707..d85d86ee6425f5 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/home/snapshot_list/snapshot_table/snapshot_table.tsx @@ -33,6 +33,21 @@ interface Props { onSnapshotDeleted: (snapshotsDeleted: Array<{ snapshot: string; repository: string }>) => void; } +const getLastSuccessfulManagedSnapshot = ( + snapshots: SnapshotDetails[] +): SnapshotDetails | undefined => { + const successfulSnapshots = snapshots + .filter( + ({ state, repository, managedRepository }) => + repository === managedRepository && state === 'SUCCESS' + ) + .sort((a, b) => { + return +new Date(b.endTime) - +new Date(a.endTime); + }); + + return successfulSnapshots[0]; +}; + export const SnapshotTable: React.FunctionComponent = ({ snapshots, repositories, @@ -49,6 +64,8 @@ export const SnapshotTable: React.FunctionComponent = ({ const { trackUiMetric } = uiMetricService; const [selectedItems, setSelectedItems] = useState([]); + const lastSuccessfulManagedSnapshot = getLastSuccessfulManagedSnapshot(snapshots); + const columns = [ { field: 'snapshot', @@ -193,21 +210,24 @@ export const SnapshotTable: React.FunctionComponent = ({ }, }, { - render: ({ snapshot, repository, isManagedRepository }: SnapshotDetails) => { + render: ({ snapshot, repository }: SnapshotDetails) => { return ( {deleteSnapshotPrompt => { - const label = !isManagedRepository + const isDeleteDisabled = Boolean(lastSuccessfulManagedSnapshot) + ? snapshot === lastSuccessfulManagedSnapshot!.snapshot + : false; + const label = isDeleteDisabled ? i18n.translate( - 'xpack.snapshotRestore.snapshotList.table.actionDeleteTooltip', - { defaultMessage: 'Delete' } - ) - : i18n.translate( 'xpack.snapshotRestore.snapshotList.table.deleteManagedRepositorySnapshotTooltip', { defaultMessage: - 'You cannot delete a snapshot stored in a managed repository.', + 'You must store the last successful snapshot in a managed repository.', } + ) + : i18n.translate( + 'xpack.snapshotRestore.snapshotList.table.actionDeleteTooltip', + { defaultMessage: 'Delete' } ); return ( @@ -215,7 +235,7 @@ export const SnapshotTable: React.FunctionComponent = ({ aria-label={i18n.translate( 'xpack.snapshotRestore.snapshotList.table.actionDeleteAriaLabel', { - defaultMessage: 'Delete snapshot `{name}`', + defaultMessage: `Delete snapshot '{name}'`, values: { name: snapshot }, } )} @@ -225,7 +245,7 @@ export const SnapshotTable: React.FunctionComponent = ({ onClick={() => deleteSnapshotPrompt([{ snapshot, repository }], onSnapshotDeleted) } - isDisabled={isManagedRepository} + isDisabled={isDeleteDisabled} /> ); @@ -265,13 +285,16 @@ export const SnapshotTable: React.FunctionComponent = ({ const selection = { onSelectionChange: (newSelectedItems: SnapshotDetails[]) => setSelectedItems(newSelectedItems), - selectable: ({ isManagedRepository }: SnapshotDetails) => !isManagedRepository, + selectable: ({ snapshot }: SnapshotDetails) => + Boolean(lastSuccessfulManagedSnapshot) + ? snapshot !== lastSuccessfulManagedSnapshot!.snapshot + : true, selectableMessage: (selectable: boolean) => { if (!selectable) { return i18n.translate( - 'xpack.snapshotRestore.snapshotList.table.deleteManagedRepositorySnapshotTooltip', + 'xpack.snapshotRestore.snapshotList.table.deleteManagedRepositorySnapshotDescription', { - defaultMessage: 'You cannot delete a snapshot stored in a managed repository.', + defaultMessage: 'You must retain the last successful snapshot in a managed repository.', } ); } diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx index 191d31cfba6293..da89807a147c39 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_add/policy_add.tsx @@ -71,6 +71,7 @@ export const PolicyAdd: React.FunctionComponent = ({ maxCount: '', minCount: '', }, + isManagedPolicy: false, }; const renderSaveError = () => { diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx index 0bfb84cef93b47..de6bedd9110033 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/sections/policy_edit/policy_edit.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle, EuiCallOut } from '@elastic/eui'; import { SlmPolicyPayload } from '../../../../common/types'; import { TIME_UNITS } from '../../../../common/constants'; @@ -51,6 +51,7 @@ export const PolicyEdit: React.FunctionComponent + <> + {policy.isManagedPolicy ? ( + <> + + } + /> + + + ) : null} + + ); }; diff --git a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts index 3f27da82bf56d1..7d44979e697a7f 100644 --- a/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/public/app/services/validation/validate_policy.ts @@ -15,10 +15,21 @@ const isStringEmpty = (str: string | null): boolean => { return str ? !Boolean(str.trim()) : true; }; -export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { +export const validatePolicy = ( + policy: SlmPolicyPayload, + validationHelperData: { + managedRepository?: { + name: string; + policy: string; + }; + isEditing?: boolean; + policyName?: string; + } +): PolicyValidation => { const i18n = textService.i18n; const { name, snapshotName, schedule, repository, config, retention } = policy; + const { managedRepository, isEditing, policyName } = validationHelperData; const validation: PolicyValidation = { isValid: true, @@ -95,6 +106,22 @@ export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => { ); } + if ( + managedRepository && + managedRepository.name === repository && + managedRepository.policy && + !(isEditing && managedRepository.policy === policyName) + ) { + validation.errors.repository.push( + i18n.translate('xpack.snapshotRestore.policyValidation.invalidRepoErrorMessage', { + defaultMessage: 'Policy "{policyName}" is already associated with this repository.', + values: { + policyName: managedRepository.policy, + }, + }) + ); + } + if (retention && retention.expireAfterValue && retention.expireAfterValue < 0) { validation.errors.expireAfterValue.push( i18n.translate( diff --git a/x-pack/legacy/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts b/x-pack/legacy/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts new file mode 100644 index 00000000000000..c86eaffa6fe117 --- /dev/null +++ b/x-pack/legacy/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// Cloud has its own system for managing SLM policies and we want to make +// this clear when Snapshot and Restore is used in a Cloud deployment. +// Retrieve the Cloud-managed policies so that UI can switch +// logical paths based on this information. +export const getManagedPolicyNames = async (callWithInternalUser: any): Promise => { + try { + const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', { + filterPath: '*.*managed_policies', + flatSettings: true, + includeDefaults: true, + }); + const { 'cluster.metadata.managed_policies': managedPolicyNames = [] } = { + ...defaults, + ...persistent, + ...transient, + }; + return managedPolicyNames; + } catch (e) { + // Silently swallow error and return empty array for managed policy names + // so that downstream calls are not blocked. In a healthy environment, we do + // not expect to reach here. + return []; + } +}; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/lib/index.ts b/x-pack/legacy/plugins/snapshot_restore/server/lib/index.ts index 6e54f997209ab8..e79a6b6c97d46c 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/lib/index.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/lib/index.ts @@ -10,4 +10,5 @@ export { } from './repository_serialization'; export { cleanSettings } from './clean_settings'; export { getManagedRepositoryName } from './get_managed_repository_name'; +export { getManagedPolicyNames } from './get_managed_policy_names'; export { deserializeRestoreShard } from './restore_serialization'; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts index c0016a4f643cd2..3b251bdd9f9902 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.test.ts @@ -48,6 +48,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { maxCount: 10, }, nextExecutionMillis: 1562722200000, + isManagedPolicy: false, }; describe('getAllHandler()', () => { diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts index f9f4daff53def3..38f9a2301af5a3 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts @@ -11,6 +11,7 @@ import { import { SlmPolicyEs, SlmPolicy, SlmPolicyPayload } from '../../../common/types'; import { deserializePolicy, serializePolicy } from '../../../common/lib'; import { Plugins } from '../../../shim'; +import { getManagedPolicyNames } from '../../lib'; let callWithInternalUser: any; @@ -34,6 +35,8 @@ export const getAllHandler: RouterRouteHandler = async ( ): Promise<{ policies: SlmPolicy[]; }> => { + const managedPolicies = await getManagedPolicyNames(callWithInternalUser); + // Get policies const policiesByName: { [key: string]: SlmPolicyEs; @@ -43,9 +46,9 @@ export const getAllHandler: RouterRouteHandler = async ( // Deserialize policies return { - policies: Object.entries(policiesByName).map(([name, policy]) => - deserializePolicy(name, policy) - ), + policies: Object.entries(policiesByName).map(([name, policy]) => { + return deserializePolicy(name, policy, managedPolicies); + }), }; }; @@ -69,9 +72,11 @@ export const getOneHandler: RouterRouteHandler = async ( throw wrapCustomError(new Error('Policy not found'), 404); } + const managedPolicies = await getManagedPolicyNames(callWithInternalUser); + // Deserialize policy return { - policy: deserializePolicy(name, policiesByName[name]), + policy: deserializePolicy(name, policiesByName[name], managedPolicies), }; }; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.test.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.test.ts index 183396ef639c15..f6717a4ddf26c9 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.test.ts @@ -46,11 +46,24 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { describe('getAllHandler()', () => { it('should arrify repositories returned from ES', async () => { - const mockEsResponse = { + const mockRepositoryEsResponse = { fooRepository: {}, barRepository: {}, }; - const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse); + + const mockPolicyEsResponse = { + my_policy: { + policy: { + repository: 'found-snapshots', + }, + }, + }; + + const callWithRequest = jest + .fn() + .mockReturnValueOnce(mockRepositoryEsResponse) + .mockReturnValueOnce(mockPolicyEsResponse); + const expectedResponse = { repositories: [ { @@ -64,7 +77,10 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { settings: {}, }, ], - managedRepository: 'found-snapshots', + managedRepository: { + name: 'found-snapshots', + policy: 'my_policy', + }, }; await expect( getAllHandler(mockRequest, callWithRequest, mockResponseToolkit) @@ -72,11 +88,26 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should return empty array if no repositories returned from ES', async () => { - const mockEsResponse = {}; - const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse); + const mockRepositoryEsResponse = {}; + const mockPolicyEsResponse = { + my_policy: { + policy: { + repository: 'found-snapshots', + }, + }, + }; + + const callWithRequest = jest + .fn() + .mockReturnValueOnce(mockRepositoryEsResponse) + .mockReturnValueOnce(mockPolicyEsResponse); + const expectedResponse = { repositories: [], - managedRepository: 'found-snapshots', + managedRepository: { + name: 'found-snapshots', + policy: 'my_policy', + }, }; await expect( getAllHandler(mockRequest, callWithRequest, mockResponseToolkit) diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts index bf4d7538b9d346..815655df283998 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -10,7 +10,12 @@ import { } from '../../../../../server/lib/create_router/error_wrappers'; import { DEFAULT_REPOSITORY_TYPES, REPOSITORY_PLUGINS_MAP } from '../../../common/constants'; -import { Repository, RepositoryType, RepositoryVerification } from '../../../common/types'; +import { + Repository, + RepositoryType, + RepositoryVerification, + SlmPolicyEs, +} from '../../../common/types'; import { Plugins } from '../../../shim'; import { @@ -34,14 +39,19 @@ export function registerRepositoriesRoutes(router: Router, plugins: Plugins) { router.delete('repositories/{names}', deleteHandler); } +interface ManagedRepository { + name?: string; + policy?: string; +} + export const getAllHandler: RouterRouteHandler = async ( req, callWithRequest ): Promise<{ repositories: Repository[]; - managedRepository?: string; + managedRepository: ManagedRepository; }> => { - const managedRepository = await getManagedRepositoryName(callWithInternalUser); + const managedRepositoryName = await getManagedRepositoryName(callWithInternalUser); const repositoriesByName = await callWithRequest('snapshot.getRepository', { repository: '_all', }); @@ -54,6 +64,35 @@ export const getAllHandler: RouterRouteHandler = async ( settings: deserializeRepositorySettings(settings), }; }); + + const managedRepository = { + name: managedRepositoryName, + } as ManagedRepository; + + // If a managed repository, we also need to check if a policy is associated to it + if (managedRepositoryName) { + try { + const policiesByName: { + [key: string]: SlmPolicyEs; + } = await callWithRequest('slm.policies', { + human: true, + }); + const managedRepositoryPolicy = Object.entries(policiesByName) + .filter(([, data]) => { + const { policy } = data; + return policy.repository === managedRepositoryName; + }) + .flat(); + + const [policyName] = managedRepositoryPolicy; + + managedRepository.policy = policyName as ManagedRepository['name']; + } catch (e) { + // swallow error for now + // we don't want to block repositories from loading if request fails + } + } + return { repositories, managedRepository }; }; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index 70c66071c1bf6d..fdd50db3091d0a 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -98,13 +98,13 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { ...defaultSnapshot, repository: 'fooRepository', snapshot: 'snapshot1', - isManagedRepository: false, + managedRepository: 'found-snapshots', }, { ...defaultSnapshot, repository: 'barRepository', snapshot: 'snapshot2', - isManagedRepository: false, + managedRepository: 'found-snapshots', }, ], }; @@ -167,7 +167,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { ...defaultSnapshot, snapshot, repository, - isManagedRepository: false, + managedRepository: 'found-snapshots', }; const response = await getOneHandler(mockOneRequest, callWithRequest, mockResponseToolkit); diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts index 1067f3e207b825..eed47b7343ec5b 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -110,8 +110,9 @@ export const getOneHandler: RouterRouteHandler = async ( ): Promise => { const { repository, snapshot } = req.params; const managedRepository = await getManagedRepositoryName(callWithInternalUser); + const { - responses: snapshotResponses, + responses: snapshotsResponse, }: { responses: Array<{ repository: string; @@ -120,19 +121,32 @@ export const getOneHandler: RouterRouteHandler = async ( }>; } = await callWithRequest('snapshot.get', { repository, - snapshot, + snapshot: '_all', + ignore_unavailable: true, }); - if (snapshotResponses && snapshotResponses[0] && snapshotResponses[0].snapshots) { - return deserializeSnapshotDetails( - repository, - snapshotResponses[0].snapshots[0], - managedRepository - ); + const snapshotsList = snapshotsResponse && snapshotsResponse[0] && snapshotsResponse[0].snapshots; + const selectedSnapshot = snapshotsList.find( + ({ snapshot: snapshotName }) => snapshot === snapshotName + ) as SnapshotDetailsEs; + + if (!selectedSnapshot) { + // If snapshot doesn't exist, manually throw 404 here + throw wrapCustomError(new Error('Snapshot not found'), 404); } - // If snapshot doesn't exist, ES will return 200 with an error object, so manually throw 404 here - throw wrapCustomError(new Error('Snapshot not found'), 404); + const successfulSnapshots = snapshotsList + .filter(({ state }) => state === 'SUCCESS') + .sort((a, b) => { + return +new Date(b.end_time) - +new Date(a.end_time); + }); + + return deserializeSnapshotDetails( + repository, + selectedSnapshot, + managedRepository, + successfulSnapshots + ); }; export const deleteHandler: RouterRouteHandler = async (req, callWithRequest) => { diff --git a/x-pack/legacy/plugins/snapshot_restore/test/fixtures/policy.ts b/x-pack/legacy/plugins/snapshot_restore/test/fixtures/policy.ts index 3dc5f78c42457e..510edb6b919f30 100644 --- a/x-pack/legacy/plugins/snapshot_restore/test/fixtures/policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/test/fixtures/policy.ts @@ -32,6 +32,7 @@ export const getPolicy = ({ snapshotName = `snapshot-${getRandomString()}`, stats = DEFAULT_STATS, version = getRandomNumber(), + isManagedPolicy = false, }: Partial = {}): SlmPolicy => ({ name, config, @@ -45,4 +46,5 @@ export const getPolicy = ({ snapshotName, stats, version, + isManagedPolicy, }); From 085a2af8ec3771c80ea2aeb8e592fd7c4c18c259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Wed, 4 Dec 2019 14:55:41 +0100 Subject: [PATCH 06/30] [APM] Fix failing ACM integration test (#52149) --- .../__snapshots__/queries.test.ts.snap | 33 ++++++--- .../agent_configuration/search.mocks.ts | 71 ------------------- .../agent_configuration/search.test.ts | 63 ---------------- .../settings/agent_configuration/search.ts | 53 +++++--------- .../routes/settings/agent_configuration.ts | 3 + 5 files changed, 44 insertions(+), 179 deletions(-) delete mode 100644 x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts delete mode 100644 x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index d64907691ef9ae..27c013ab4c6d38 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -55,16 +55,26 @@ Object { "minimum_should_match": 2, "should": Array [ Object { - "term": Object { - "service.name": Object { - "value": "foo", + "constant_score": Object { + "boost": 2, + "filter": Object { + "term": Object { + "service.name": Object { + "value": "foo", + }, + }, }, }, }, Object { - "term": Object { - "service.environment": Object { - "value": "bar", + "constant_score": Object { + "boost": 1, + "filter": Object { + "term": Object { + "service.environment": Object { + "value": "bar", + }, + }, }, }, }, @@ -106,9 +116,14 @@ Object { "minimum_should_match": 2, "should": Array [ Object { - "term": Object { - "service.name": Object { - "value": "foo", + "constant_score": Object { + "boost": 2, + "filter": Object { + "term": Object { + "service.name": Object { + "value": "foo", + }, + }, }, }, }, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts deleted file mode 100644 index 1ed7f56e0b10d3..00000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts +++ /dev/null @@ -1,71 +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. - */ - -export const searchMocks = { - took: 1, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0 - }, - hits: { - total: { - value: 3, - relation: 'eq' - }, - max_score: 0.9808292, - hits: [ - { - _index: '.apm-agent-configuration', - _id: '-aQHsm0BxZLczArvNQYW', - _score: 0.9808292, - _source: { - service: { - environment: 'production' - }, - settings: { - transaction_sample_rate: 0.3 - }, - '@timestamp': 1570649879829, - applied_by_agent: false, - etag: 'c511f4c1df457371c4446c9c4925662e18726f51' - } - }, - { - _index: '.apm-agent-configuration', - _id: '-KQHsm0BxZLczArvNAb0', - _score: 0.18232156, - _source: { - service: { - name: 'my_service' - }, - settings: { - transaction_sample_rate: 0.2 - }, - '@timestamp': 1570649879795, - applied_by_agent: false, - etag: 'a13cd8fee5a2fcc2ae773a60a4deaf7f76b90a65' - } - }, - { - _index: '.apm-agent-configuration', - _id: '96QHsm0BxZLczArvNAbD', - _score: 0.0, - _source: { - service: {}, - settings: { - transaction_sample_rate: 0.1 - }, - '@timestamp': 1570649879743, - applied_by_agent: false, - etag: 'c7f4ba16f00a9c9bf3c49024c5b6d4632ff05ff5' - } - } - ] - } -}; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts deleted file mode 100644 index dcf7329b229d85..00000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts +++ /dev/null @@ -1,63 +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 { searchConfigurations } from './search'; -import { searchMocks } from './search.mocks'; -import { Setup } from '../../helpers/setup_request'; - -describe('search configurations', () => { - it('should return configuration by matching on service.name', async () => { - const res = await searchConfigurations({ - serviceName: 'my_service', - environment: 'production', - setup: ({ - config: { get: () => '' }, - client: { search: async () => searchMocks }, - internalClient: { search: async () => searchMocks }, - indices: { - apm_oss: { - sourcemapIndices: 'myIndex', - errorIndices: 'myIndex', - onboardingIndices: 'myIndex', - spanIndices: 'myIndex', - transactionIndices: 'myIndex', - metricsIndices: 'myIndex', - apmAgentConfigurationIndex: 'myIndex' - } - } - } as unknown) as Setup - }); - - expect(res!._source.service).toEqual({ name: 'my_service' }); - expect(res!._source.settings).toEqual({ transaction_sample_rate: 0.2 }); - }); - - it('should return configuration by matching on "production" env', async () => { - const res = await searchConfigurations({ - serviceName: 'non_existing_service', - environment: 'production', - setup: ({ - config: { get: () => '' }, - client: { search: async () => searchMocks }, - internalClient: { search: async () => searchMocks }, - indices: { - apm_oss: { - sourcemapIndices: 'myIndex', - errorIndices: 'myIndex', - onboardingIndices: 'myIndex', - spanIndices: 'myIndex', - transactionIndices: 'myIndex', - metricsIndices: 'myIndex', - apmAgentConfigurationIndex: 'myIndex' - } - } - } as unknown) as Setup - }); - - expect(res!._source.service).toEqual({ environment: 'production' }); - expect(res!._source.settings).toEqual({ transaction_sample_rate: 0.3 }); - }); -}); diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts index a02dd7af755e0f..766baead006b6e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts @@ -21,17 +21,20 @@ export async function searchConfigurations({ setup: Setup; }) { const { internalClient, indices } = setup; - - // sorting order - // 1. exact match: service.name AND service.environment (eg. opbeans-node / production) - // 2. Partial match: service.name and no service.environment (eg. opbeans-node / All) - // 3. Partial match: service.environment and no service.name (eg. All / production) - // 4. Catch all: no service.name and no service.environment (eg. All / All) - const environmentFilter = environment - ? [{ term: { [SERVICE_ENVIRONMENT]: { value: environment } } }] + ? [ + { + constant_score: { + filter: { term: { [SERVICE_ENVIRONMENT]: { value: environment } } }, + boost: 1 + } + } + ] : []; + // In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring) + // Additionally a boost has been added to service.name to ensure it scores higher + // if there is tie between a config with a matching service.name and a config with a matching environment const params = { index: indices.apmAgentConfigurationIndex, body: { @@ -39,7 +42,12 @@ export async function searchConfigurations({ bool: { minimum_should_match: 2, should: [ - { term: { [SERVICE_NAME]: { value: serviceName } } }, + { + constant_score: { + filter: { term: { [SERVICE_NAME]: { value: serviceName } } }, + boost: 2 + } + }, ...environmentFilter, { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }, { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } } @@ -52,33 +60,6 @@ export async function searchConfigurations({ const resp = await internalClient.search( params ); - const { hits } = resp.hits; - - const exactMatch = hits.find( - hit => - hit._source.service.name === serviceName && - hit._source.service.environment === environment - ); - - if (exactMatch) { - return exactMatch; - } - - const matchWithServiceName = hits.find( - hit => hit._source.service.name === serviceName - ); - - if (matchWithServiceName) { - return matchWithServiceName; - } - - const matchWithEnvironment = hits.find( - hit => hit._source.service.environment === environment - ); - - if (matchWithEnvironment) { - return matchWithEnvironment; - } return resp.hits.hits[0] as ESSearchHit | undefined; } diff --git a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts index b897dfb4b91235..a6709110790403 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings/agent_configuration.ts @@ -170,6 +170,9 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ }); if (!config) { + context.logger.info( + `Config was not found for ${body.service.name}/${body.service.environment}` + ); throw new Boom('Not found', { statusCode: 404 }); } From 73651a1b28437db3596c516cf301cb5cd1e8667f Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Wed, 4 Dec 2019 05:59:53 -0800 Subject: [PATCH 07/30] Disabled actions (#51975) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: disable actions from SIEM by using `disabledActions` list * feat: filter out actions specified in `disabledActions` input prop * test: 💍 remove legacy test * chore: 🤖 remove unused import * test: 💍 add disabledActions prop tests --- .../public/lib/embeddables/i_embeddable.ts | 5 + .../lib/panel/embeddable_panel.test.tsx | 102 +++++++++++++++++- .../public/lib/panel/embeddable_panel.tsx | 21 ++-- .../components/embeddables/embedded_map.tsx | 13 +-- .../embeddables/embedded_map_helpers.test.tsx | 10 +- .../embeddables/embedded_map_helpers.tsx | 26 +---- 6 files changed, 125 insertions(+), 52 deletions(-) diff --git a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts index 0b864e1a3573be..33cb146a056cbc 100644 --- a/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts +++ b/src/plugins/embeddable/public/lib/embeddables/i_embeddable.ts @@ -28,6 +28,11 @@ export interface EmbeddableInput { id: string; lastReloadRequestTime?: number; hidePanelTitles?: boolean; + + /** + * List of action IDs that this embeddable should not render. + */ + disabledActions?: string[]; } export interface EmbeddableOutput { diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 9eed400daf9c9b..196d6f934134bf 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -25,7 +25,7 @@ import { nextTick } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; -import { IAction, ITrigger } from 'src/plugins/ui_actions/public'; +import { IAction, ITrigger, IUiActionsApi } from 'src/plugins/ui_actions/public'; import { Trigger, GetEmbeddableFactory, ViewMode } from '../types'; import { EmbeddableFactory, isErrorEmbeddable } from '../embeddables'; import { EmbeddablePanel } from './embeddable_panel'; @@ -42,6 +42,7 @@ import { } from '../test_samples/embeddables/contact_card/contact_card_embeddable'; // eslint-disable-next-line import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; +import { EuiBadge } from '@elastic/eui'; const actionRegistry = new Map(); const triggerRegistry = new Map(); @@ -174,6 +175,105 @@ test('HelloWorldContainer in view mode hides edit mode actions', async () => { expect(findTestSubject(component, `embeddablePanelAction-${editModeAction.id}`).length).toBe(0); }); +const renderInEditModeAndOpenContextMenu = async ( + embeddableInputs: any, + getActions: IUiActionsApi['getTriggerCompatibleActions'] = () => Promise.resolve([]) +) => { + const inspector = inspectorPluginMock.createStartContract(); + + const container = new HelloWorldContainer({ id: '123', panels: {}, viewMode: ViewMode.VIEW }, { + getEmbeddableFactory, + } as any); + + const embeddable = await container.addNewEmbeddable< + ContactCardEmbeddableInput, + ContactCardEmbeddableOutput, + ContactCardEmbeddable + >(CONTACT_CARD_EMBEDDABLE, embeddableInputs); + + const component = mount( + + []) as any} + getEmbeddableFactory={(() => undefined) as any} + notifications={{} as any} + overlays={{} as any} + inspector={inspector} + SavedObjectFinder={() => null} + /> + + ); + + findTestSubject(component, 'embeddablePanelToggleMenuIcon').simulate('click'); + await nextTick(); + component.update(); + + return { component }; +}; + +test('HelloWorldContainer in edit mode hides disabledActions', async () => { + const action = { + id: 'FOO', + type: 'FOO', + getIconType: () => undefined, + getDisplayName: () => 'foo', + isCompatible: async () => true, + execute: async () => {}, + }; + const getActions = () => Promise.resolve([action]); + + const { component: component1 } = await renderInEditModeAndOpenContextMenu( + { + firstName: 'Bob', + }, + getActions + ); + const { component: component2 } = await renderInEditModeAndOpenContextMenu( + { + firstName: 'Bob', + disabledActions: ['FOO'], + }, + getActions + ); + + const fooContextMenuActionItem1 = findTestSubject(component1, 'embeddablePanelAction-FOO'); + const fooContextMenuActionItem2 = findTestSubject(component2, 'embeddablePanelAction-FOO'); + + expect(fooContextMenuActionItem1.length).toBe(1); + expect(fooContextMenuActionItem2.length).toBe(0); +}); + +test('HelloWorldContainer hides disabled badges', async () => { + const action = { + id: 'BAR', + type: 'BAR', + getIconType: () => undefined, + getDisplayName: () => 'bar', + isCompatible: async () => true, + execute: async () => {}, + }; + const getActions = () => Promise.resolve([action]); + + const { component: component1 } = await renderInEditModeAndOpenContextMenu( + { + firstName: 'Bob', + }, + getActions + ); + const { component: component2 } = await renderInEditModeAndOpenContextMenu( + { + firstName: 'Bob', + disabledActions: ['BAR'], + }, + getActions + ); + + expect(component1.find(EuiBadge).length).toBe(1); + expect(component2.find(EuiBadge).length).toBe(0); +}); + test('HelloWorldContainer in edit mode shows edit mode actions', async () => { const inspector = inspectorPluginMock.createStartContract(); diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx index 2b48bf237829c0..234d8508bb97a4 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.tsx @@ -91,15 +91,19 @@ export class EmbeddablePanel extends React.Component { } private async refreshBadges() { - const badges = await this.props.getActions(PANEL_BADGE_TRIGGER, { + let badges: IAction[] = await this.props.getActions(PANEL_BADGE_TRIGGER, { embeddable: this.props.embeddable, }); + if (!this.mounted) return; - if (this.mounted) { - this.setState({ - badges, - }); + const { disabledActions } = this.props.embeddable.getInput(); + if (disabledActions) { + badges = badges.filter(badge => disabledActions.indexOf(badge.id) === -1); } + + this.setState({ + badges, + }); } public UNSAFE_componentWillMount() { @@ -200,10 +204,15 @@ export class EmbeddablePanel extends React.Component { }; private getActionContextMenuPanel = async () => { - const actions = await this.props.getActions(CONTEXT_MENU_TRIGGER, { + let actions = await this.props.getActions(CONTEXT_MENU_TRIGGER, { embeddable: this.props.embeddable, }); + const { disabledActions } = this.props.embeddable.getInput(); + if (disabledActions) { + actions = actions.filter(action => disabledActions.indexOf(action.id) === -1); + } + const createGetUserData = (overlays: OverlayStart) => async function getUserData(context: { embeddable: IEmbeddable }) { return new Promise<{ title: string | undefined }>(resolve => { diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx index 5a3a689e12d833..cb73cf73b8d06c 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -23,7 +23,7 @@ import { Loader } from '../loader'; import { useStateToaster } from '../toasters'; import { Embeddable } from './embeddable'; import { EmbeddableHeader } from './embeddable_header'; -import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers'; +import { createEmbeddable, displayErrorToast } from './embedded_map_helpers'; import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; import { MapToolTip } from './map_tool_tip/map_tool_tip'; import * as i18n from './translations'; @@ -104,17 +104,6 @@ export const EmbeddedMapComponent = ({ const plugins = useKibanaPlugins(); const core = useKibanaCore(); - // Setup embeddables API (i.e. detach extra actions) useEffect - useEffect(() => { - try { - setupEmbeddablesAPI(plugins); - } catch (e) { - displayErrorToast(i18n.ERROR_CONFIGURING_EMBEDDABLES_API, e.message, dispatchToaster); - setIsLoading(false); - setIsError(true); - } - }, []); - // Initial Load useEffect useEffect(() => { let isSubscribed = true; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx index a2c3e2cc288d7c..b4b2b98ddb8d6b 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createEmbeddable, displayErrorToast, setupEmbeddablesAPI } from './embedded_map_helpers'; +import { createEmbeddable, displayErrorToast } from './embedded_map_helpers'; import { createUiNewPlatformMock } from 'ui/new_platform/__mocks__/helpers'; import { createPortalNode } from 'react-reverse-portal'; -import { PluginsStart } from 'ui/new_platform/new_platform'; jest.mock('ui/new_platform'); jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -45,13 +44,6 @@ describe('embedded_map_helpers', () => { }); }); - describe('setupEmbeddablesAPI', () => { - test('detaches extra UI actions', () => { - setupEmbeddablesAPI((npStart.plugins as unknown) as PluginsStart); - expect(npStart.plugins.uiActions.detachAction).toHaveBeenCalledTimes(2); - }); - }); - describe('createEmbeddable', () => { test('attaches refresh action', async () => { const setQueryMock = jest.fn(); diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 03c4492b77f1be..b9a9df9824eee4 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -7,14 +7,8 @@ import uuid from 'uuid'; import React from 'react'; import { OutPortal, PortalNode } from 'react-reverse-portal'; -import { PluginsStart } from 'ui/new_platform/new_platform'; - import { ActionToaster, AppToast } from '../toasters'; -import { - CONTEXT_MENU_TRIGGER, - PANEL_BADGE_TRIGGER, - ViewMode, -} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { ViewMode } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { IndexPatternMapping, MapEmbeddable, @@ -53,23 +47,6 @@ export const displayErrorToast = ( }); }; -/** - * Temporary Embeddables API configuration override until ability to edit actions is addressed: - * https://github.com/elastic/kibana/issues/43643 - * - * @param plugins new platform plugins - * - * @throws Error if trigger/action doesn't exist - */ -export const setupEmbeddablesAPI = (plugins: PluginsStart) => { - try { - plugins.uiActions.detachAction(CONTEXT_MENU_TRIGGER, 'CUSTOM_TIME_RANGE'); - plugins.uiActions.detachAction(PANEL_BADGE_TRIGGER, 'CUSTOM_TIME_RANGE_BADGE'); - } catch (e) { - throw e; - } -}; - /** * Creates MapEmbeddable with provided initial configuration * @@ -115,6 +92,7 @@ export const createEmbeddable = async ( openTOCDetails: [], hideFilterActions: false, mapCenter: { lon: -1.05469, lat: 15.96133, zoom: 1 }, + disabledActions: ['CUSTOM_TIME_RANGE', 'CUSTOM_TIME_RANGE_BADGE'], }; const renderTooltipContent = ({ From 95e5edd9c4413bdb97fcf189a631df0cf1d13f89 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 4 Dec 2019 15:21:07 +0100 Subject: [PATCH 08/30] Instrument Kibana with Elastic APM (#43548) Instruments Kibana with Elastic APM by adding the Node.js agent to the source code. The agent is not turned on by default but can be enabled by setting the environment variable `ELASTIC_APM_ACTIVE=true` or by creating an apm config file called `config/apm.dev.js` and setting `active: true` inside of it. This implementation is not meant to be used by end-users of Kibana as it lacks integration with the regular Kibana config file. For now, this is meant as a useful internal tool for Elastic employees when developing Kibana. By default, it's pre-configured with a `serverUrl` pointing to an APM Server hosted on Elastic Cloud. The data is stored in an ES cluster accessible only by Elastic employees. These defaults can easily be overwritten using environment variables or via the custom config file. --- .gitignore | 1 + CONTRIBUTING.md | 16 + config/apm.js | 81 ++++ package.json | 1 + scripts/kibana.js | 1 + src/apm.js | 39 ++ src/cli/index.js | 1 + .../server/http/base_path_proxy_server.ts | 3 + src/dev/build/tasks/copy_source_task.js | 1 + yarn.lock | 371 +++++++++++++++++- 10 files changed, 510 insertions(+), 5 deletions(-) create mode 100644 config/apm.js create mode 100644 src/apm.js diff --git a/.gitignore b/.gitignore index 02b20da297fc67..e7391a5c292d0e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ disabledPlugins webpackstats.json /config/* !/config/kibana.yml +!/config/apm.js coverage selenium .babel_register_cache.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e2a8459c2b01ab..53e44fbede7247 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,7 @@ A high level overview of our contributing guidelines. - [Internationalization](#internationalization) - [Testing and Building](#testing-and-building) - [Debugging server code](#debugging-server-code) + - [Instrumenting with Elastic APM](#instrumenting-with-elastic-apm) - [Debugging Unit Tests](#debugging-unit-tests) - [Unit Testing Plugins](#unit-testing-plugins) - [Cross-browser compatibility](#cross-browser-compatibility) @@ -374,6 +375,21 @@ macOS users on a machine with a discrete graphics card may see significant speed ### Debugging Server Code `yarn debug` will start the server with Node's inspect flag. Kibana's development mode will start three processes on ports `9229`, `9230`, and `9231`. Chrome's developer tools need to be configured to connect to all three connections. Add `localhost:` for each Kibana process in Chrome's developer tools connection tab. +### Instrumenting with Elastic APM +Kibana ships with the [Elastic APM Node.js Agent](https://github.com/elastic/apm-agent-nodejs) built-in for debugging purposes. + +Its default configuration is meant to be used by core Kibana developers only, but it can easily be re-configured to your needs. +In its default configuration it's disabled and will, once enabled, send APM data to a centrally managed Elasticsearch cluster accessible only to Elastic employees. + +To change the location where data is sent, use the [`serverUrl`](https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html#server-url) APM config option. +To activate the APM agent, use the [`active`](https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html#active) APM config option. + +All config options can be set either via environment variables, or by creating an appropriate config file under `config/apm.dev.js`. +For more information about configuring the APM agent, please refer to [the documentation](https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuring-the-agent.html). + +Once the agent is active, it will trace all incoming HTTP requests to Kibana, monitor for errors, and collect process-level metrics. +The collected data will be sent to the APM Server and is viewable in the APM UI in Kibana. + ### Unit testing frameworks Kibana is migrating unit testing from Mocha to Jest. Legacy unit tests still exist in Mocha but all new unit tests should be written in Jest. Mocha tests diff --git a/config/apm.js b/config/apm.js new file mode 100644 index 00000000000000..8efbbf87487e36 --- /dev/null +++ b/config/apm.js @@ -0,0 +1,81 @@ +/* + * 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. + */ + +/** + * DO NOT EDIT THIS FILE! + * + * This file contains the configuration for the Elastic APM instrumentaion of + * Kibana itself and is only intented to be used during development of Kibana. + * + * Instrumentation is turned off by default. Once activated it will send APM + * data to an Elasticsearch cluster accessible by Elastic employees. + * + * To modify the configuration, either use environment variables, or create a + * file named `config/apm.dev.js`, which exports a config object as described + * in the docs. + * + * For an overview over the available configuration files, see: + * https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html + * + * For general information about Elastic APM, see: + * https://www.elastic.co/guide/en/apm/get-started/current/index.html + */ + +const { readFileSync } = require('fs'); +const { join } = require('path'); +const { execSync } = require('child_process'); +const merge = require('lodash.merge'); + +module.exports = merge({ + active: false, + serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', + // The secretToken below is intended to be hardcoded in this file even though + // it makes it public. This is not a security/privacy issue. Normally we'd + // instead disable the need for a secretToken in the APM Server config where + // the data is transmitted to, but due to how it's being hosted, it's easier, + // for now, to simply leave it in. + secretToken: 'R0Gjg46pE9K9wGestd', + globalLabels: {}, + centralConfig: false, + logUncaughtExceptions: true +}, devConfig()); + +const rev = gitRev(); +if (rev !== null) module.exports.globalLabels.git_rev = rev; + +try { + const filename = join(__dirname, '..', 'data', 'uuid'); + module.exports.globalLabels.kibana_uuid = readFileSync(filename, 'utf-8'); +} catch (e) {} // eslint-disable-line no-empty + +function gitRev() { + try { + return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); + } catch (e) { + return null; + } +} + +function devConfig() { + try { + return require('./apm.dev'); // eslint-disable-line import/no-unresolved + } catch (e) { + return {}; + } +} diff --git a/package.json b/package.json index ea6276496e84da..98ba41772ed60a 100644 --- a/package.json +++ b/package.json @@ -159,6 +159,7 @@ "d3-cloud": "1.2.5", "deepmerge": "^4.2.2", "del": "^5.1.0", + "elastic-apm-node": "^3.2.0", "elasticsearch": "^16.5.0", "elasticsearch-browser": "^16.5.0", "encode-uri-query": "1.0.1", diff --git a/scripts/kibana.js b/scripts/kibana.js index b1b470a37535fc..f5a63e6c07dd60 100644 --- a/scripts/kibana.js +++ b/scripts/kibana.js @@ -17,5 +17,6 @@ * under the License. */ +require('../src/apm')(process.env.ELASTIC_APM_PROXY_SERVICE_NAME || 'kibana-proxy'); require('../src/setup_node_env'); require('../src/cli/cli'); diff --git a/src/apm.js b/src/apm.js new file mode 100644 index 00000000000000..04a70ee71c53ec --- /dev/null +++ b/src/apm.js @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +const { existsSync } = require('fs'); +const { join } = require('path'); +const { name, version } = require('../package.json'); + +module.exports = function (serviceName = name) { + if (process.env.kbnWorkerType === 'optmzr') return; + + const conf = { + serviceName: `${serviceName}-${version.replace(/\./g, '_')}` + }; + + if (configFileExists()) conf.configFile = 'config/apm.js'; + else conf.active = false; + + require('elastic-apm-node').start(conf); +}; + +function configFileExists() { + return existsSync(join(__dirname, '..', 'config', 'apm.js')); +} diff --git a/src/cli/index.js b/src/cli/index.js index 4af5e3c68423cd..45f88eaf82a5b8 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -17,5 +17,6 @@ * under the License. */ +require('../apm')(); require('../setup_node_env'); require('./cli'); diff --git a/src/core/server/http/base_path_proxy_server.ts b/src/core/server/http/base_path_proxy_server.ts index cde35f3cbe995a..276e3955a4678b 100644 --- a/src/core/server/http/base_path_proxy_server.ts +++ b/src/core/server/http/base_path_proxy_server.ts @@ -17,6 +17,8 @@ * under the License. */ +import apm from 'elastic-apm-node'; + import { ByteSizeValue } from '@kbn/config-schema'; import { Server, Request } from 'hapi'; import Url from 'url'; @@ -139,6 +141,7 @@ export class BasePathProxyServer { // Before we proxy request to a target port we may want to wait until some // condition is met (e.g. until target listener is ready). async (request, responseToolkit) => { + apm.setTransactionName(`${request.method.toUpperCase()} /{basePath}/{kbnPath*}`); await blockUntil(); return responseToolkit.continue; }, diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js index e487ac0567f766..a693c6ce8a8a6f 100644 --- a/src/dev/build/tasks/copy_source_task.js +++ b/src/dev/build/tasks/copy_source_task.js @@ -46,6 +46,7 @@ export const CopySourceTask = { 'typings/**', 'webpackShims/**', 'config/kibana.yml', + 'config/apm.js', 'tsconfig*.json', '.i18nrc.json', 'kibana.d.ts' diff --git a/yarn.lock b/yarn.lock index bda9f056bdf5c7..76bc070e27842d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4674,6 +4674,11 @@ affine-hull@^1.0.0: dependencies: robust-orientation "^1.1.3" +after-all-results@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/after-all-results/-/after-all-results-2.0.0.tgz#6ac2fc202b500f88da8f4f5530cfa100f4c6a2d0" + integrity sha1-asL8ICtQD4jaj09VMM+hAPTGotA= + after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" @@ -5702,6 +5707,13 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +async-cache@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/async-cache/-/async-cache-1.1.0.tgz#4a9a5a89d065ec5d8e5254bd9ee96ba76c532b5a" + integrity sha1-SppaidBl7F2OUlS9nulrp2xTK1o= + dependencies: + lru-cache "^4.0.0" + async-done@^1.2.0, async-done@^1.2.2: version "1.3.2" resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.3.2.tgz#5e15aa729962a4b07414f528a88cdf18e0b290a2" @@ -5746,6 +5758,18 @@ async-settle@^1.0.0: dependencies: async-done "^1.2.2" +async-value-promise@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/async-value-promise/-/async-value-promise-1.1.1.tgz#68957819e3eace804f3b4b69477e2bd276c15378" + integrity sha512-c2RFDKjJle1rHa0YxN9Ysu97/QBu3Wa+NOejJxsX+1qVDJrkD3JL/GN1B3gaILAEXJXbu/4Z1lcoCHFESe/APA== + dependencies: + async-value "^1.2.2" + +async-value@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/async-value/-/async-value-1.2.2.tgz#84517a1e7cb6b1a5b5e181fa31be10437b7fb125" + integrity sha1-hFF6Hny2saW14YH6Mb4QQ3t/sSU= + async@1.x, async@^1.4.2, async@^1.5.2, async@~1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" @@ -5829,6 +5853,11 @@ autoprefixer@9.6.1, autoprefixer@^9.4.9: postcss "^7.0.17" postcss-value-parser "^4.0.0" +await-event@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/await-event/-/await-event-2.1.0.tgz#78e9f92684bae4022f9fa0b5f314a11550f9aa76" + integrity sha1-eOn5JoS65AIvn6C18xShFVD5qnY= + aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" @@ -6508,6 +6537,13 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +basic-auth@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + batch-processor@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" @@ -6584,6 +6620,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== +binary-search@^1.3.3: + version "1.3.6" + resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" + integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== + binaryextensions@2: version "2.1.1" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935" @@ -6853,6 +6894,13 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" +breadth-filter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/breadth-filter/-/breadth-filter-2.0.0.tgz#7b3f8737f46ba1946aec19355ecf5df2bdb7e47c" + integrity sha512-thQShDXnFWSk2oVBixRCyrWsFoV5tfOpWKHmxwafHQDNxCfDBk539utpvytNjmlFrTMqz41poLwJvA1MW3z0MQ== + dependencies: + object.entries "^1.0.4" + brfs@^1.3.0, brfs@^1.4.0: version "1.4.3" resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.4.3.tgz#db675d6f5e923e6df087fca5859c9090aaed3216" @@ -8537,6 +8585,11 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= +console-log-level@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/console-log-level/-/console-log-level-1.4.1.tgz#9c5a6bb9ef1ef65b05aba83028b0ff894cdf630a" + integrity sha512-VZzbIORbP+PPcN/gg3DXClTLPLg5Slwd5fL2MIc+o1qZ4BXBvWyc6QxPk6T/Mkr6IVjRpoAGf32XxP3ZWMVRcQ== + const-max-uint32@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/const-max-uint32/-/const-max-uint32-1.0.2.tgz#f009bb6230e678ed874dd2d6a9cd9e3cbfabb676" @@ -8575,6 +8628,11 @@ constate@^1.2.0: resolved "https://registry.yarnpkg.com/constate/-/constate-1.3.2.tgz#fa5f0fc292207f1ec21b46a5eb81f59c8b0a8b84" integrity sha512-aaILV4vXwGTUZaQZHS5F1xBV8wRCR0Ow1505fdkS5/BPg6hbQrhNqdHL4wgxWgaDeEj43mu/Fb+LhqOKTMcrgQ== +container-info@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/container-info/-/container-info-1.0.1.tgz#6b383cb5e197c8d921e88983388facb04124b56b" + integrity sha512-wk/+uJvPHOFG+JSwQS+fw6H6yw3Oyc8Kw9L4O2MN817uA90OqJ59nlZbbLPqDudsjJ7Tetee3pwExdKpd2ahjQ== + contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" @@ -8650,7 +8708,7 @@ cookie@0.3.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= -cookie@0.4.0: +cookie@0.4.0, cookie@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== @@ -8744,7 +8802,7 @@ core-js@^3.0.1, core-js@^3.0.4, core-js@^3.2.1: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw== -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= @@ -10625,6 +10683,56 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== +elastic-apm-http-client@^9.2.0: + version "9.2.1" + resolved "https://registry.yarnpkg.com/elastic-apm-http-client/-/elastic-apm-http-client-9.2.1.tgz#e0e980ceb9975ff770bdbf2f5cdaac39fd70e8e6" + integrity sha512-KythghGrgsozTVZdsUdKED1+IcfN1CEIWS4zL8crsV234Dj9QaffG88E7pu11PZ04HiOSVemAKby21aNRV0kLQ== + dependencies: + breadth-filter "^2.0.0" + container-info "^1.0.1" + end-of-stream "^1.4.4" + fast-safe-stringify "^2.0.7" + fast-stream-to-buffer "^1.0.0" + pump "^3.0.0" + readable-stream "^3.4.0" + stream-chopper "^3.0.1" + unicode-byte-truncate "^1.0.0" + +elastic-apm-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-3.2.0.tgz#a1aa5b255f8867788b38e2854aa331c3a0cc0e22" + integrity sha512-GnoYcge/Xy8/I/0pHF2V9tZU1aRFMqcP7PQ0WXCqdETMUUq7Gmf0fcdzjA8CoDALR+rfsjn4ByF1p7uXoTJoPQ== + dependencies: + after-all-results "^2.0.0" + async-value-promise "^1.1.1" + basic-auth "^2.0.1" + console-log-level "^1.4.1" + cookie "^0.4.0" + core-util-is "^1.0.2" + elastic-apm-http-client "^9.2.0" + end-of-stream "^1.4.1" + fast-safe-stringify "^2.0.7" + http-headers "^3.0.2" + http-request-to-url "^1.0.0" + is-native "^1.0.1" + measured-reporting "^1.51.1" + monitor-event-loop-delay "^1.0.0" + object-filter-sequence "^1.0.0" + object-identity-map "^1.0.2" + original-url "^1.2.3" + read-pkg-up "^7.0.0" + redact-secrets "^1.0.0" + relative-microtime "^2.0.0" + require-ancestors "^1.0.0" + require-in-the-middle "^5.0.0" + semver "^6.1.1" + set-cookie-serde "^1.0.0" + shallow-clone-shim "^1.0.0" + sql-summary "^1.0.1" + stackman "^4.0.0" + traceparent "^1.0.0" + unicode-byte-truncate "^1.0.0" + elasticsearch-browser@^16.5.0: version "16.5.0" resolved "https://registry.yarnpkg.com/elasticsearch-browser/-/elasticsearch-browser-16.5.0.tgz#d2efbbf8751bb563e91b74117a14b9211df5cfe9" @@ -10736,7 +10844,7 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -end-of-stream@^1.4.1: +end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -10922,6 +11030,11 @@ errno@^0.1.1, errno@^0.1.3, errno@~0.1.7: dependencies: prr "~1.0.1" +error-callsites@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/error-callsites/-/error-callsites-2.0.2.tgz#55c17a9490a85d72158563f13dc078851ca05b1e" + integrity sha512-s35ELWAKAY9oPqnnfP1V4AnasWV0r2ihaLlpsCGrykZgcR/YKsMXV3q8Ap4Mmp8U90VxJqxKJE5Io0IkkRhJIg== + error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" @@ -12159,6 +12272,18 @@ fast-safe-stringify@^2.0.4: resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz#04b26106cc56681f51a044cfc0d76cf0008ac2c2" integrity sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg== +fast-safe-stringify@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + +fast-stream-to-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-stream-to-buffer/-/fast-stream-to-buffer-1.0.0.tgz#793340cc753e7ec9c7fb6d57a53a0b911cb0f588" + integrity sha512-bI/544WUQlD2iXBibQbOMSmG07Hay7YrpXlKaeGTPT7H7pC0eitt3usak5vUwEvCGK/O7rUAM3iyQValGU22TQ== + dependencies: + end-of-stream "^1.4.1" + fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" @@ -12842,6 +12967,11 @@ formsy-react@^1.1.5: form-data-to-object "^0.2.0" prop-types "^15.5.10" +forwarded-parse@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.0.tgz#1ae9d7a4be3af884f74d936d856f7d8c6abd0439" + integrity sha512-as9a7Xelt0CvdUy7/qxrY73dZq2vMx49F556fwjjFrUyzq5uHHfeLgD2cCq/6P4ZvusGZzjD6aL2NdgGdS5Cew== + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -13234,6 +13364,11 @@ get-own-enumerable-property-symbols@^3.0.0: resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz#b877b49a5c16aefac3655f2ed2ea5b684df8d203" integrity sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg== +get-own-property-descriptors-polyfill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-own-property-descriptors-polyfill/-/get-own-property-descriptors-polyfill-1.0.1.tgz#e0814a5c32bd9ef387a1de44147f93056a904002" + integrity sha512-S1k3UgpTshd171qaPldcr+BY82277tsNI+ETIZLJ/re6KYQYbV4qRtUw5kmHIZlEy4hZvwdzHFn+8xupNVl4YQ== + get-port@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" @@ -14953,6 +15088,13 @@ http-errors@1.7.2, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-headers@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/http-headers/-/http-headers-3.0.2.tgz#5147771292f0b39d6778d930a3a59a76fc7ef44d" + integrity sha512-87E1I+2Wg4dxxz4rcxElo3dxO/w1ZtgL1yA0Sb6vH3qU16vRKq1NjWQv9SCY3ly2OQROcoxHZOUpmelS+k6wOw== + dependencies: + next-line "^1.1.0" + http-https@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/http-https/-/http-https-1.0.0.tgz#2f908dd5f1db4068c058cd6e6d4ce392c913389b" @@ -14998,6 +15140,14 @@ http-proxy@^1.17.0: follow-redirects "^1.0.0" requires-port "^1.0.0" +http-request-to-url@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/http-request-to-url/-/http-request-to-url-1.0.0.tgz#e56b9418f79f29d344fed05cfe2c56ccb8cc79ac" + integrity sha512-YYx0lKXG9+T1fT2q3ZgXLczMI3jW09g9BvIA6L3BG0tFqGm83Ka/+RUZGANRG7Ut/yueD7LPcZQ/+pA5ndNajw== + dependencies: + await-event "^2.1.0" + socket-location "^1.0.0" + http-signature@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" @@ -15973,6 +16123,13 @@ is-installed-globally@0.1.0, is-installed-globally@^0.1.0: global-dirs "^0.1.0" is-path-inside "^1.0.0" +is-integer@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-integer/-/is-integer-1.0.7.tgz#6bde81aacddf78b659b6629d629cadc51a886d5c" + integrity sha1-a96Bqs3feLZZtmKdYpytxRqIbVw= + dependencies: + is-finite "^1.0.0" + is-invalid-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-invalid-path/-/is-invalid-path-0.1.0.tgz#307a855b3cf1a938b44ea70d2c61106053714f34" @@ -16003,6 +16160,14 @@ is-my-json-valid@^2.10.0: jsonpointer "^4.0.0" xtend "^4.0.0" +is-native@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-native/-/is-native-1.0.1.tgz#cd18cc162e8450d683b5babe79ac99c145449675" + integrity sha1-zRjMFi6EUNaDtbq+eayZwUVElnU= + dependencies: + is-nil "^1.0.0" + to-source-code "^1.0.0" + is-natural-number@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" @@ -16013,6 +16178,11 @@ is-negated-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= +is-nil@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-nil/-/is-nil-1.0.1.tgz#2daba29e0b585063875e7b539d071f5b15937969" + integrity sha1-LauingtYUGOHXntTnQcfWxWTeWk= + is-npm@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" @@ -16209,6 +16379,11 @@ is-scoped@^1.0.0: dependencies: scoped-regex "^1.0.0" +is-secret@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-secret/-/is-secret-1.2.1.tgz#04b9ca1880ea763049606cfe6c2a08a93f33abe3" + integrity sha512-VtBantcgKL2a64fDeCmD1JlkHToh3v0bVOhyJZ5aGTjxtCgrdNcjaC9GaaRFXi19gA4/pYFpnuyoscIgQCFSMQ== + is-ssh@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.1.tgz#f349a8cadd24e65298037a522cf7520f2e81a0f3" @@ -18033,6 +18208,15 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" +load-source-map@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/load-source-map/-/load-source-map-1.0.0.tgz#318f49905ce8a709dfb7cc3f16f3efe3bcf1dd05" + integrity sha1-MY9JkFzopwnft8w/FvPv47zx3QU= + dependencies: + in-publish "^2.0.0" + semver "^5.3.0" + source-map "^0.5.6" + loader-runner@^2.3.0, loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" @@ -18742,6 +18926,11 @@ mapbox-gl@1.3.1: tinyqueue "^2.0.0" vt-pbf "^3.1.1" +mapcap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mapcap/-/mapcap-1.0.0.tgz#e8e29d04a160eaf8c92ec4bcbd2c5d07ed037e5a" + integrity sha512-KcNlZSlFPx+r1jYZmxEbTVymG+dIctf10WmWkuhrhrblM+KMoF77HelwihL5cxYlORye79KoR4IlOOk99lUJ0g== + marge@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/marge/-/marge-1.0.1.tgz#52d6026911e62e1dd1cf60a07313dde285a8370c" @@ -18849,6 +19038,24 @@ mdurl@^1.0.1: resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= +measured-core@^1.51.1: + version "1.51.1" + resolved "https://registry.yarnpkg.com/measured-core/-/measured-core-1.51.1.tgz#98989705c00bfb0d8a20e665a9f8d6e246a40518" + integrity sha512-DZQP9SEwdqqYRvT2slMK81D/7xwdxXosZZBtLVfPSo6y5P672FBTbzHVdN4IQyUkUpcVOR9pIvtUy5Ryl7NKyg== + dependencies: + binary-search "^1.3.3" + optional-js "^2.0.0" + +measured-reporting@^1.51.1: + version "1.51.1" + resolved "https://registry.yarnpkg.com/measured-reporting/-/measured-reporting-1.51.1.tgz#6aeb209ad55edf3940e8afa75c8f97f541216b31" + integrity sha512-JCt+2u6XT1I5lG3SuYqywE0e62DJuAzBcfMzWGUhIYtPQV2Vm4HiYt/durqmzsAbZV181CEs+o/jMKWJKkYIWw== + dependencies: + console-log-level "^1.4.1" + mapcap "^1.0.0" + measured-core "^1.51.1" + optional-js "^2.0.0" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -19506,6 +19713,11 @@ module-definition@^3.0.0, module-definition@^3.1.0: ast-module-types "^2.4.0" node-source-walk "^4.0.0" +module-details-from-path@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" + integrity sha1-EUyUlnPiqKNenTV4hSeqN7Z52is= + module-lookup-amd@^6.1.0: version "6.2.0" resolved "https://registry.yarnpkg.com/module-lookup-amd/-/module-lookup-amd-6.2.0.tgz#70600008b3f26630fde9ef9ae6165ac69de6ecbb" @@ -19545,6 +19757,11 @@ monaco-editor@~0.17.0: resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.17.1.tgz#8fbe96ca54bfa75262706e044f8f780e904aa45c" integrity sha512-JAc0mtW7NeO+0SwPRcdkfDbWLgkqL9WfP1NbpP9wNASsW6oWqgZqNIWt4teymGjZIXTElx3dnQmUYHmVrJ7HxA== +monitor-event-loop-delay@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/monitor-event-loop-delay/-/monitor-event-loop-delay-1.0.0.tgz#b5ab78165a3bb93f2b275c50d01430c7f155d1f7" + integrity sha512-YRIr1exCIfBDLZle8WHOfSo7Xg3M+phcZfq9Fx1L6Abo+atGp7cge5pM7PjyBn4s1oZI/BRD4EMrzQBbPpVb5Q== + monocle-ts@^1.0.0: version "1.7.1" resolved "https://registry.yarnpkg.com/monocle-ts/-/monocle-ts-1.7.1.tgz#03a615938aa90983a4fa29749969d30f72d80ba1" @@ -19810,6 +20027,11 @@ newtype-ts@^0.2.4: fp-ts "^1.0.0" monocle-ts "^1.0.0" +next-line@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-line/-/next-line-1.1.0.tgz#fcae57853052b6a9bae8208e40dd7d3c2d304603" + integrity sha1-/K5XhTBStqm66CCOQN19PC0wRgM= + next-tick@1: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" @@ -20400,11 +20622,23 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-filter-sequence@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/object-filter-sequence/-/object-filter-sequence-1.0.0.tgz#10bb05402fff100082b80d7e83991b10db411692" + integrity sha512-CsubGNxhIEChNY4cXYuA6KXafztzHqzLLZ/y3Kasf3A+sa3lL9thq3z+7o0pZqzEinjXT6lXDPAfVWI59dUyzQ== + object-hash@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== +object-identity-map@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object-identity-map/-/object-identity-map-1.0.2.tgz#2b4213a4285ca3a8cd2e696782c9964f887524e7" + integrity sha512-a2XZDGyYTngvGS67kWnqVdpoaJWsY7C1GhPJvejWAFCsUioTAaiTu8oBad7c6cI4McZxr4CmvnZeycK05iav5A== + dependencies: + object.entries "^1.1.0" + object-inspect@^1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" @@ -20675,6 +20909,11 @@ optimist@^0.6.1, optimist@~0.6.1: minimist "~0.0.1" wordwrap "~0.0.2" +optional-js@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/optional-js/-/optional-js-2.1.1.tgz#c2dc519ad119648510b4d241dbb60b1167c36a46" + integrity sha512-mUS4bDngcD5kKzzRUd1HVQkr9Lzzby3fSrrPR9wOHhQiyYo+hDS5NVli5YQzGjQRQ15k5Sno4xH9pfykJdeEUA== + optionator@^0.8.1, optionator@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" @@ -20726,6 +20965,13 @@ ordered-read-streams@^1.0.0: dependencies: readable-stream "^2.0.1" +original-url@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/original-url/-/original-url-1.2.3.tgz#133aff4b2d27e38a98d736f7629c56262b7153e1" + integrity sha512-BYm+pKYLtS4mVe/mgT3YKGtWV5HzN/XKiaIu1aK4rsxyjuHeTW9N+xVBEpJcY1onB3nccfH0RbzUEoimMqFUHQ== + dependencies: + forwarded-parse "^2.1.0" + original@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" @@ -22533,6 +22779,11 @@ randexp@0.4.6: discontinuous-range "1.0.0" ret "~0.1.10" +random-poly-fill@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/random-poly-fill/-/random-poly-fill-1.0.1.tgz#13634dc0255a31ecf85d4a182d92c40f9bbcf5ed" + integrity sha512-bMOL0hLfrNs52+EHtIPIXxn2PxYwXb0qjnKruTjXiM/sKfYqj506aB2plFwWW1HN+ri724bAVVGparh4AtlJKw== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" @@ -23512,6 +23763,15 @@ read-pkg-up@^6.0.0: read-pkg "^5.1.1" type-fest "^0.5.0" +read-pkg-up@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.0.tgz#3f3e53858ec5ae5e6fe14bc479da0a7c98f85ff3" + integrity sha512-t2ODkS/vTTcRlKwZiZsaLGb5iwfx9Urp924aGzVyboU6+7Z2i6eGr/G1Z4mjvwLLQV3uFOBKobNRGM3ux2PD/w== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -23699,6 +23959,14 @@ recursive-readdir@2.2.2: dependencies: minimatch "3.0.4" +redact-secrets@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redact-secrets/-/redact-secrets-1.0.0.tgz#60f1db56924fe90a203ba8ccb39283cdbb0d907c" + integrity sha1-YPHbVpJP6QogO6jMs5KDzbsNkHw= + dependencies: + is-secret "^1.0.0" + traverse "^0.6.6" + redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -23979,6 +24247,11 @@ relateurl@0.2.x: resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= +relative-microtime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/relative-microtime/-/relative-microtime-2.0.0.tgz#cceed2af095ecd72ea32011279c79e5fcc7de29b" + integrity sha512-l18ha6HEZc+No/uK4GyAnNxgKW7nvEe35IaeN54sShMojtqik2a6GbTyuiezkjpPaqP874Z3lW5ysBo5irz4NA== + relative@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/relative/-/relative-3.0.2.tgz#0dcd8ec54a5d35a3c15e104503d65375b5a5367f" @@ -24269,6 +24542,11 @@ request@^2.87.0: tunnel-agent "^0.6.0" uuid "^3.1.0" +require-ancestors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/require-ancestors/-/require-ancestors-1.0.0.tgz#807831f8f8081fb12863da81ddb15c8f2a73a004" + integrity sha512-Nqeo9Gfp0KvnxTixnxLGEbThMAi+YYgnwRoigtOs1Oo3eGBYfqCd3dagq1vBCVVuc1EnIt3Eu1eGemwOOEZozw== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -24279,6 +24557,15 @@ require-from-string@^2.0.1: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +require-in-the-middle@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.0.2.tgz#ce3593007a61583b39ccdcd2c167a2a326c670b2" + integrity sha512-l2r6F9i6t5xp4OE9cw/daB/ooQKHZOOW1AYPADhEvk/Tj/THJDS8gePp76Zyuht6Cj57a0KL+eHK5Dyv7wZnKA== + dependencies: + debug "^4.1.1" + module-details-from-path "^1.0.3" + resolve "^1.12.0" + require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" @@ -25075,7 +25362,7 @@ semver@^6.1.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.1.tgz#53f53da9b30b2103cd4f15eab3a18ecbcb210c9b" integrity sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ== -semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -25190,6 +25477,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-cookie-serde@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-cookie-serde/-/set-cookie-serde-1.0.0.tgz#bcf9c260ed2212ac4005a53eacbaaa37c07ac452" + integrity sha512-Vq8e5GsupfJ7okHIvEPcfs5neCo7MZ1ZuWrO3sllYi3DOWt6bSSCpADzqXjz3k0fXehnoFIrmmhty9IN6U6BXQ== + set-getter@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" @@ -25240,6 +25532,13 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +shallow-clone-shim@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallow-clone-shim/-/shallow-clone-shim-1.1.0.tgz#c1048ba9167f313f4f4c019ff3f0a40626322960" + integrity sha512-ZY+sf7fm8CDFecoL/IntHFhqu8Ll+elOcuXO5WlVgSfnpxuUMni/Y9sB9gMf85nWsVDM+CfMJpLBwiN/lOO5/w== + dependencies: + get-own-property-descriptors-polyfill "^1.0.1" + shallow-clone@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" @@ -25516,6 +25815,13 @@ sntp@2.x.x: dependencies: hoek "4.x.x" +socket-location@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/socket-location/-/socket-location-1.0.0.tgz#6f0c6f891c9a61c9a750265c14921d12196d266f" + integrity sha512-TwxpRM0pPE/3b24XQGLx8zq2J8kOwTy40FtiNC1KrWvl/Tsf7RYXruE9icecMhQwicXMo/HUJlGap8DNt2cgYw== + dependencies: + await-event "^2.1.0" + socket.io-adapter@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b" @@ -25851,6 +26157,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +sql-summary@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sql-summary/-/sql-summary-1.0.1.tgz#a2dddb5435bae294eb11424a7330dc5bafe09c2b" + integrity sha512-IpCr2tpnNkP3Jera4ncexsZUp0enJBLr+pHCyTweMUBrbJsTgQeLWx1FXLhoBj/MvcnUQpkgOn2EY8FKOkUzww== + squel@^5.13.0: version "5.13.0" resolved "https://registry.yarnpkg.com/squel/-/squel-5.13.0.tgz#09cc73e91f0d0e326482605ee76e3b7ac881ddf6" @@ -25901,6 +26212,17 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" integrity sha1-1PM6tU6OOHeLDKXP07OvsS22hiA= +stackman@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/stackman/-/stackman-4.0.0.tgz#3ccdc8682fee36373ed2492dc3dad546eb44647d" + integrity sha512-JHhUxla4KkXVzPRJoBdIolVbXWBv2qIUe/XdsH9/fkXCgsIdFhCny91tqy9Zld66ROj+dZ0E54l/I3vL3y3Uiw== + dependencies: + after-all-results "^2.0.0" + async-cache "^1.1.0" + debug "^4.1.1" + error-callsites "^2.0.2" + load-source-map "^1.0.0" + state-toggle@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425" @@ -26024,6 +26346,13 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" +stream-chopper@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/stream-chopper/-/stream-chopper-3.0.1.tgz#73791ae7bf954c297d6683aec178648efc61dd75" + integrity sha512-f7h+ly8baAE26iIjcp3VbnBkbIRGtrvV0X0xxFM/d7fwLTYnLzDPTXRKNxa2HZzohOrc96NTrR+FaV3mzOelNA== + dependencies: + readable-stream "^3.0.6" + stream-each@^1.1.0: version "1.2.3" resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" @@ -27250,6 +27579,13 @@ to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +to-source-code@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/to-source-code/-/to-source-code-1.0.2.tgz#dd136bdb1e1dbd80bbeacf088992678e9070bfea" + integrity sha1-3RNr2x4dvYC76s8IiZJnjpBwv+o= + dependencies: + is-nil "^1.0.0" + to-through@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" @@ -27317,12 +27653,19 @@ tr46@^1.0.0, tr46@^1.0.1: dependencies: punycode "^2.1.0" +traceparent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/traceparent/-/traceparent-1.0.0.tgz#9b14445cdfe5c19f023f1c04d249c3d8e003a5ce" + integrity sha512-b/hAbgx57pANQ6cg2eBguY3oxD6FGVLI1CC2qoi01RmHR7AYpQHPXTig9FkzbWohEsVuHENZHP09aXuw3/LM+w== + dependencies: + random-poly-fill "^1.0.1" + traverse-chain@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1" integrity sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE= -traverse@0.6.6, traverse@~0.6.6: +traverse@0.6.6, traverse@^0.6.6, traverse@~0.6.6: version "0.6.6" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" integrity sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc= @@ -27985,6 +28328,11 @@ type-fest@^0.6.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + type-is@~1.6.15, type-is@~1.6.16: version "1.6.16" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" @@ -28154,6 +28502,14 @@ unherit@^1.0.4: inherits "^2.0.1" xtend "^4.0.1" +unicode-byte-truncate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unicode-byte-truncate/-/unicode-byte-truncate-1.0.0.tgz#aa6f0f3475193fe20c320ac9213e36e62e8764a7" + integrity sha1-qm8PNHUZP+IMMgrJIT425i6HZKc= + dependencies: + is-integer "^1.0.6" + unicode-substring "^0.1.0" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -28185,6 +28541,11 @@ unicode-property-aliases-ecmascript@^1.0.4: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" integrity sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg== +unicode-substring@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicode-substring/-/unicode-substring-0.1.0.tgz#6120ce3c390385dbcd0f60c32b9065c4181d4b36" + integrity sha1-YSDOPDkDhdvND2DDK5BlxBgdSzY= + unicode-trie@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-0.3.1.tgz#d671dddd89101a08bac37b6a5161010602052085" From 57865e43ad590e3088a4bf5b1423c09a4f4aa145 Mon Sep 17 00:00:00 2001 From: Dmitry Lemeshko Date: Wed, 4 Dec 2019 15:24:42 +0100 Subject: [PATCH 09/30] Functional tests: elastic chart provider (#52085) * adding elastic-chart service * update visual test --- test/functional/page_objects/discover_page.js | 4 ++ test/functional/services/elastic_chart.ts | 55 +++++++++++++++++++ test/functional/services/index.ts | 2 + .../tests/discover/chart_visualization.js | 18 +++--- 4 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 test/functional/services/elastic_chart.ts diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 8d12b2117a2149..28d7526388b40f 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -31,6 +31,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { const config = getService('config'); const defaultFindTimeout = config.get('timeouts.find'); const comboBox = getService('comboBox'); + const elasticChart = getService('elasticChart'); class DiscoverPage { async getQueryField() { @@ -292,6 +293,9 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { await testSubjects.missingOrFail('filterSelectionPanel', { allowHidden: true }); } + async waitForChartLoadingComplete(renderCount) { + await elasticChart.waitForRenderingCount('discoverChart', renderCount); + } } return new DiscoverPage(); diff --git a/test/functional/services/elastic_chart.ts b/test/functional/services/elastic_chart.ts new file mode 100644 index 00000000000000..4f4dbcba5f0b87 --- /dev/null +++ b/test/functional/services/elastic_chart.ts @@ -0,0 +1,55 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../ftr_provider_context'; + +export function ElasticChartProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const log = getService('log'); + + class ElasticChart { + public async waitForRenderComplete(dataTestSubj: string) { + const chart = await testSubjects.find(dataTestSubj); + const rendered = await chart.findAllByCssSelector('.echChart[data-ech-render-complete=true]'); + expect(rendered).to.equal( + 1, + `Rendering for elastic-chart with data-test-subj='${dataTestSubj}' was not finished in time` + ); + } + + public async getVisualizationRenderingCount(dataTestSubj: string) { + const chart = await testSubjects.find(dataTestSubj); + const visContainer = await chart.findByCssSelector('.echChart'); + const renderingCount = await visContainer.getAttribute('data-ech-render-count'); + return Number(renderingCount); + } + + public async waitForRenderingCount(dataTestSubj: string, previousCount = 1) { + await retry.waitFor(`rendering count to be equal to [${previousCount + 1}]`, async () => { + const currentRenderingCount = await this.getVisualizationRenderingCount(dataTestSubj); + log.debug(`-- currentRenderingCount=${currentRenderingCount}`); + return currentRenderingCount === previousCount + 1; + }); + } + } + + return new ElasticChart(); +} diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 6098e9931f299f..ea47ccf1d2704c 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -31,6 +31,7 @@ import { // @ts-ignore not TS yet } from './dashboard'; import { DocTableProvider } from './doc_table'; +import { ElasticChartProvider } from './elastic_chart'; import { EmbeddingProvider } from './embedding'; import { FailureDebuggingProvider } from './failure_debugging'; import { FilterBarProvider } from './filter_bar'; @@ -81,4 +82,5 @@ export const services = { globalNav: GlobalNavProvider, toasts: ToastsProvider, savedQueryManagementComponent: SavedQueryManagementComponentProvider, + elasticChart: ElasticChartProvider, }; diff --git a/test/visual_regression/tests/discover/chart_visualization.js b/test/visual_regression/tests/discover/chart_visualization.js index c90f29c66acb89..da6b7ff50e8f49 100644 --- a/test/visual_regression/tests/discover/chart_visualization.js +++ b/test/visual_regression/tests/discover/chart_visualization.js @@ -27,7 +27,6 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); const visualTesting = getService('visualTesting'); - const find = getService('find'); const defaultSettings = { defaultIndex: 'logstash-*', 'discover:sampleSize': 1 @@ -54,7 +53,7 @@ export default function ({ getService, getPageObjects }) { it('should show bars in the correct time zone', async function () { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); - await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); + await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -64,7 +63,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Hourly'); - await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); + await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -74,7 +73,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Daily'); - await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); + await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -84,7 +83,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Weekly'); - await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); + await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -98,7 +97,7 @@ export default function ({ getService, getPageObjects }) { }); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); - await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); + await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -108,7 +107,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Monthly'); - await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); + await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -118,7 +117,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Yearly'); - await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); + await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -128,7 +127,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.setChartInterval('Auto'); - await find.byCssSelector(`.echChart[data-ech-render-count="${++renderCounter}"]`); + await PageObjects.discover.waitForChartLoadingComplete(++renderCounter); await visualTesting.snapshot({ show: ['discoverChart'], }); @@ -143,6 +142,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.discover.waitForChartLoadingComplete(1); await visualTesting.snapshot({ show: ['discoverChart'], }); From 686afd7ce0b3195b64f290c26923792e30f072d1 Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Wed, 4 Dec 2019 08:04:10 -0700 Subject: [PATCH 10/30] [ML] DF Analytics: create classification jobs via the UI (#51619) * wip: classification job config in form * ability to create classification job in form + validation * ensure classification types are correct for validation * update reducer test to include jobType validity check * update analytics jobs help text * update newJobCapsService to support boolean fields --- .../data_frame_analytics/common/analytics.ts | 24 +++++++++++++- .../components/analytics_list/actions.tsx | 9 +++++- .../create_analytics_form.tsx | 31 ++++++++++++++----- .../create_analytics_form/job_type.tsx | 15 +++++++-- .../use_create_analytics_form/reducer.test.ts | 3 +- .../use_create_analytics_form/reducer.ts | 18 ++++++++--- .../hooks/use_create_analytics_form/state.ts | 9 ++++-- .../job_service/new_job_caps/field_service.ts | 1 + 8 files changed, 90 insertions(+), 20 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 33b84bbe1caefa..1d3f26dfd88eca 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -31,6 +31,14 @@ interface RegressionAnalysis { }; } +interface ClassificationAnalysis { + classification: { + dependent_variable: string; + training_percent?: number; + num_top_classes?: string; + }; +} + export const SEARCH_SIZE = 1000; export const defaultSearchQuery = { @@ -77,11 +85,16 @@ interface LoadEvaluateResult { error: string | null; } -type AnalysisConfig = OutlierAnalysis | RegressionAnalysis | GenericAnalysis; +type AnalysisConfig = + | OutlierAnalysis + | RegressionAnalysis + | ClassificationAnalysis + | GenericAnalysis; export enum ANALYSIS_CONFIG_TYPE { OUTLIER_DETECTION = 'outlier_detection', REGRESSION = 'regression', + CLASSIFICATION = 'classification', UNKNOWN = 'unknown', } @@ -100,6 +113,10 @@ export const getDependentVar = (analysis: AnalysisConfig) => { if (isRegressionAnalysis(analysis)) { depVar = analysis.regression.dependent_variable; } + + if (isClassificationAnalysis(analysis)) { + depVar = analysis.classification.dependent_variable; + } return depVar; }; @@ -132,6 +149,11 @@ export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => { return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION; }; +export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; +}; + export const isRegressionResultsSearchBoolQuery = ( arg: any ): arg is RegressionResultsSearchBoolQuery => { diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 5e5283f9e6c49b..f3da4839d4b977 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -13,7 +13,11 @@ import { createPermissionFailureMessage, } from '../../../../../privilege/check_privilege'; -import { getAnalysisType } from '../../../../common/analytics'; +import { + getAnalysisType, + isRegressionAnalysis, + isOutlierAnalysis, +} from '../../../../common/analytics'; import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; import { stopAnalytics } from '../../services/analytics_service'; @@ -26,10 +30,13 @@ export const AnalyticsViewAction = { render: (item: DataFrameAnalyticsListRow) => { const analysisType = getAnalysisType(item.config.analysis); const jobStatus = item.stats.state; + const isDisabled = + !isRegressionAnalysis(item.config.analysis) && !isOutlierAnalysis(item.config.analysis); const url = getResultsUrl(item.id, analysisType, jobStatus); return ( (window.location.href = url)} size="xs" color="text" diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index 9cbdc457e75cae..e478087c2a8cc8 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -54,6 +54,8 @@ const NUMERICAL_FIELD_TYPES = new Set([ 'scaled_float', ]); +const SUPPORTED_CLASSIFICATION_FIELD_TYPES = new Set(['boolean', 'text', 'keyword', 'ip']); + // List of system fields we want to ignore for the numeric field check. const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; @@ -112,6 +114,18 @@ export const CreateAnalyticsForm: FC = ({ actions, sta } }; + // Regression supports numeric fields. Classification supports numeric, boolean, text, keyword and ip. + const shouldAddFieldOption = (field: Field) => { + if (field.id === EVENT_RATE_FIELD_ID) return false; + + const isNumerical = NUMERICAL_FIELD_TYPES.has(field.type); + const isSupportedByClassification = + isNumerical || SUPPORTED_CLASSIFICATION_FIELD_TYPES.has(field.type); + + if (jobType === JOB_TYPES.REGRESSION) return isNumerical; + if (jobType === JOB_TYPES.CLASSIFICATION) return isNumerical || isSupportedByClassification; + }; + const debouncedMmlEstimateLoad = debounce(async () => { try { const jobConfig = getJobConfigFromFormState(form); @@ -146,12 +160,12 @@ export const CreateAnalyticsForm: FC = ({ actions, sta if (indexPattern !== undefined) { await newJobCapsService.initializeFromIndexPattern(indexPattern); - // Get fields and filter for numeric + // Get fields and filter for supported types for job type const { fields } = newJobCapsService; const options: Array<{ label: string }> = []; fields.forEach((field: Field) => { - if (NUMERICAL_FIELD_TYPES.has(field.type) && field.id !== EVENT_RATE_FIELD_ID) { + if (shouldAddFieldOption(field)) { options.push({ label: field.id }); } }); @@ -196,7 +210,10 @@ export const CreateAnalyticsForm: FC = ({ actions, sta }; useEffect(() => { - if (jobType === JOB_TYPES.REGRESSION && sourceIndexNameEmpty === false) { + if ( + (jobType === JOB_TYPES.REGRESSION || jobType === JOB_TYPES.CLASSIFICATION) && + sourceIndexNameEmpty === false + ) { loadDependentFieldOptions(); } else if (jobType === JOB_TYPES.OUTLIER_DETECTION && sourceIndexNameEmpty === false) { validateSourceIndexFields(); @@ -206,11 +223,11 @@ export const CreateAnalyticsForm: FC = ({ actions, sta useEffect(() => { const hasBasicRequiredFields = jobType !== undefined && sourceIndex !== '' && sourceIndexNameValid === true; + const jobTypesWithDepVar = + jobType === JOB_TYPES.REGRESSION || jobType === JOB_TYPES.CLASSIFICATION; const hasRequiredAnalysisFields = - (jobType === JOB_TYPES.REGRESSION && - dependentVariable !== '' && - trainingPercent !== undefined) || + (jobTypesWithDepVar && dependentVariable !== '' && trainingPercent !== undefined) || jobType === JOB_TYPES.OUTLIER_DETECTION; if (hasBasicRequiredFields && hasRequiredAnalysisFields) { @@ -406,7 +423,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta isInvalid={!destinationIndexNameEmpty && !destinationIndexNameValid} /> - {jobType === JOB_TYPES.REGRESSION && ( + {(jobType === JOB_TYPES.REGRESSION || jobType === JOB_TYPES.CLASSIFICATION) && ( >; } export const JobType: FC = ({ type, setFormState }) => { @@ -21,7 +21,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.outlierDetectionHelpText', { defaultMessage: - 'Outlier detection jobs require a source index that is mapped as a table-like data structure and will only analyze numeric and boolean fields. Please use the advanced editor to apply custom options such as the model memory limit and analysis type.', + 'Outlier detection jobs require a source index that is mapped as a table-like data structure and will only analyze numeric and boolean fields. Please use the advanced editor to add custom options to the configuration.', } ); @@ -29,13 +29,22 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.outlierRegressionHelpText', { defaultMessage: - 'Regression jobs will only analyze numeric fields. Please use the advanced editor to apply custom options such as the model memory limit and prediction field name.', + 'Regression jobs will only analyze numeric fields. Please use the advanced editor to apply custom options such as the prediction field name.', + } + ); + + const classificationHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.create.classificationHelpText', + { + defaultMessage: + 'Classification jobs require a source index that is mapped as a table-like data structure and supports fields that are numeric, boolean, text, keyword or ip. Please use the advanced editor to apply custom options such as the prediction field name.', } ); const helpText = { outlier_detection: outlierHelpText, regression: regressionHelpText, + classification: classificationHelpText, }; return ( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts index cabf0946ce8713..7db9420396a9a8 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -10,7 +10,7 @@ import { DataFrameAnalyticsConfig } from '../../../../common'; import { ACTION } from './actions'; import { reducer, validateAdvancedEditor } from './reducer'; -import { getInitialState } from './state'; +import { getInitialState, JOB_TYPES } from './state'; jest.mock('ui/index_patterns', () => ({ validateIndexPattern: () => true, @@ -51,6 +51,7 @@ describe('useCreateAnalyticsForm', () => { destinationIndex: 'the-destination-index', jobId: 'the-analytics-job-id', sourceIndex: 'the-source-index', + jobType: JOB_TYPES.OUTLIER_DETECTION, }, }); expect(updatedState.isValid).toBe(true); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 56d09169a3c390..918c42f480e1e5 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -22,7 +22,11 @@ import { JOB_ID_MAX_LENGTH, ALLOWED_DATA_UNITS, } from '../../../../../../../common/constants/validation'; -import { getDependentVar, isRegressionAnalysis } from '../../../../common/analytics'; +import { + getDependentVar, + isRegressionAnalysis, + isClassificationAnalysis, +} from '../../../../common/analytics'; const mmlAllowedUnitsStr = `${ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join( ', ' @@ -53,7 +57,7 @@ const getSourceIndexString = (state: State) => { }; export const validateAdvancedEditor = (state: State): State => { - const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern } = state.form; + const { jobIdEmpty, jobIdValid, jobIdExists, createIndexPattern } = state.form; const { jobConfig } = state; state.advancedEditorMessages = []; @@ -89,9 +93,9 @@ export const validateAdvancedEditor = (state: State): State => { } let dependentVariableEmpty = false; - if (isRegressionAnalysis(jobConfig.analysis)) { + if (isRegressionAnalysis(jobConfig.analysis) || isClassificationAnalysis(jobConfig.analysis)) { const dependentVariableName = getDependentVar(jobConfig.analysis) || ''; - dependentVariableEmpty = jobType === JOB_TYPES.REGRESSION && dependentVariableName === ''; + dependentVariableEmpty = dependentVariableName === ''; } if (sourceIndexNameEmpty) { @@ -201,7 +205,10 @@ const validateForm = (state: State): State => { modelMemoryLimit, } = state.form; - const dependentVariableEmpty = jobType === JOB_TYPES.REGRESSION && dependentVariable === ''; + const jobTypeEmpty = jobType === undefined; + const dependentVariableEmpty = + (jobType === JOB_TYPES.REGRESSION || jobType === JOB_TYPES.CLASSIFICATION) && + dependentVariable === ''; const modelMemoryLimitEmpty = modelMemoryLimit === ''; if (!modelMemoryLimitEmpty && modelMemoryLimit !== undefined) { @@ -210,6 +217,7 @@ const validateForm = (state: State): State => { } state.isValid = + !jobTypeEmpty && state.form.modelMemoryLimitUnitValid && !jobIdEmpty && jobIdValid && diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index f911b5a45e158c..64bd3681afce92 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -14,6 +14,7 @@ export enum DEFAULT_MODEL_MEMORY_LIMIT { regression = '100mb', // eslint-disable-next-line @typescript-eslint/camelcase outlier_detection = '50mb', + classification = '100mb', } export type EsIndexName = string; @@ -34,6 +35,7 @@ export interface FormMessage { export enum JOB_TYPES { OUTLIER_DETECTION = 'outlier_detection', REGRESSION = 'regression', + CLASSIFICATION = 'classification', } export interface State { @@ -149,9 +151,12 @@ export const getJobConfigFromFormState = ( model_memory_limit: formState.modelMemoryLimit, }; - if (formState.jobType === JOB_TYPES.REGRESSION) { + if ( + formState.jobType === JOB_TYPES.REGRESSION || + formState.jobType === JOB_TYPES.CLASSIFICATION + ) { jobConfig.analysis = { - regression: { + [formState.jobType]: { dependent_variable: formState.dependentVariable, training_percent: formState.trainingPercent, }, diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts index af4d869274d73a..3cfb5521890628 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job_caps/field_service.ts @@ -33,6 +33,7 @@ const supportedTypes: string[] = [ ES_FIELD_TYPES.IP, ES_FIELD_TYPES.GEO_POINT, ES_FIELD_TYPES.GEO_SHAPE, + ES_FIELD_TYPES.BOOLEAN, ]; export function fieldServiceProvider( From 43b97d8a056db70c997f7b73c4e98da34af8d786 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 4 Dec 2019 16:17:19 +0100 Subject: [PATCH 11/30] Clean up uses of deprecated API's in node core (#51431) Ensure no deprecated Node.js core API's are used in Kibana. This is achieved by throwing an error in either development mode or in CI if one of the deprecated API's is called, and as such, new PR's should no longer be able to be merged if they use deprecated API's. Some of these API's (like the `Buffer` constructor`) is a security risk. --- package.json | 5 +- .../kbn-babel-code-parser/src/strategies.js | 17 +- .../tasks/build/rewrite_package_json.js | 12 +- .../http/integration_tests/router.test.ts | 2 +- src/dev/ci_setup/setup_env.sh | 2 + .../__tests__/proxy_config_collection.js | 2 +- .../__jest__/step_time_field.test.js | 1 + .../usage/telemetry_usage_collector.test.ts | 2 +- .../series_functions/__tests__/yaxis.js | 8 +- src/legacy/server/sass/build.js | 38 +- .../ui/public/chrome/__mocks__/index.js | 50 +++ .../utils/deep_clone_with_buffers.test.ts | 4 +- src/legacy/utils/deep_clone_with_buffers.ts | 2 +- .../dynamic_dll_plugin/dll_compiler.js | 11 +- .../actions/replace_panel_action.test.tsx | 23 +- .../embeddable/grid/dashboard_grid.test.tsx | 21 +- .../viewport/dashboard_viewport.test.tsx | 8 +- .../query_string_input.test.tsx.snap | 408 ++++++++++++++++++ .../query_bar_top_row.test.tsx | 4 +- .../query_string_input.test.tsx | 2 + .../saved_object_finder.test.tsx | 2 + utilities/visual_regression.js | 34 +- .../__test__/createErrorGroupWatch.test.ts | 2 +- .../index_management/__mocks__/ui/notify.js | 2 + .../public/components/report_info_button.tsx | 1 - .../server/lib/esqueue/__tests__/worker.js | 24 +- .../index_privileges.test.tsx.snap | 2 +- .../privileges/es/index_privileges.test.tsx | 14 +- .../privileges/es/index_privileges.tsx | 4 +- .../__tests__/get_xpack.js | 14 +- x-pack/package.json | 2 +- .../authentication/authenticator.test.ts | 32 +- yarn.lock | 90 +--- 33 files changed, 638 insertions(+), 207 deletions(-) diff --git a/package.json b/package.json index 98ba41772ed60a..be8ef669e76c0e 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "typespec": "typings-tester --config x-pack/legacy/plugins/canvas/public/lib/aeroelastic/tsconfig.json x-pack/legacy/plugins/canvas/public/lib/aeroelastic/__fixtures__/typescript/typespec_tests.ts", "checkLicenses": "node scripts/check_licenses --dev", "build": "node scripts/build --all-platforms", - "start": "node --trace-warnings --trace-deprecation scripts/kibana --dev ", + "start": "node --trace-warnings --throw-deprecation scripts/kibana --dev", "debug": "node --nolazy --inspect scripts/kibana --dev", "debug-break": "node --nolazy --inspect-brk scripts/kibana --dev", "karma": "karma start", @@ -406,7 +406,6 @@ "gulp-sourcemaps": "2.6.5", "has-ansi": "^3.0.0", "iedriver": "^3.14.1", - "image-diff": "1.6.3", "intl-messageformat-parser": "^1.4.0", "is-path-inside": "^2.1.0", "istanbul-instrumenter-loader": "3.0.1", @@ -434,7 +433,7 @@ "node-sass": "^4.9.4", "normalize-path": "^3.0.0", "nyc": "^14.1.1", - "pixelmatch": "4.0.2", + "pixelmatch": "^5.1.0", "pkg-up": "^2.0.0", "pngjs": "^3.4.0", "postcss": "^7.0.5", diff --git a/packages/kbn-babel-code-parser/src/strategies.js b/packages/kbn-babel-code-parser/src/strategies.js index 89621bc53bd534..f116abde9e0e6d 100644 --- a/packages/kbn-babel-code-parser/src/strategies.js +++ b/packages/kbn-babel-code-parser/src/strategies.js @@ -20,6 +20,7 @@ import { canRequire } from './can_require'; import { dependenciesVisitorsGenerator } from './visitors'; import { dirname, isAbsolute, resolve } from 'path'; +import { builtinModules } from 'module'; export function _calculateTopLevelDependency(inputDep, outputDep = '') { // The path separator will be always the forward slash @@ -48,14 +49,18 @@ export function _calculateTopLevelDependency(inputDep, outputDep = '') { return _calculateTopLevelDependency(depSplitPaths.join(pathSeparator), outputDep); } -export async function dependenciesParseStrategy(cwd, parseSingleFile, mainEntry, wasParsed, results) { - // Retrieve native nodeJS modules - const natives = process.binding('natives'); - +export async function dependenciesParseStrategy( + cwd, + parseSingleFile, + mainEntry, + wasParsed, + results +) { // Get dependencies from a single file and filter // out node native modules from the result - const dependencies = (await parseSingleFile(mainEntry, dependenciesVisitorsGenerator)) - .filter(dep => !natives[dep]); + const dependencies = (await parseSingleFile(mainEntry, dependenciesVisitorsGenerator)).filter( + dep => !builtinModules.includes(dep) + ); // Return the list of all the new entries found into // the current mainEntry that we could use to look for diff --git a/packages/kbn-plugin-helpers/tasks/build/rewrite_package_json.js b/packages/kbn-plugin-helpers/tasks/build/rewrite_package_json.js index db33b209951ebd..64656baee6fd2b 100644 --- a/packages/kbn-plugin-helpers/tasks/build/rewrite_package_json.js +++ b/packages/kbn-plugin-helpers/tasks/build/rewrite_package_json.js @@ -41,19 +41,9 @@ module.exports = function rewritePackage(buildSource, buildVersion, kibanaVersio delete pkg.scripts; delete pkg.devDependencies; - file.contents = toBuffer(JSON.stringify(pkg, null, 2)); + file.contents = Buffer.from(JSON.stringify(pkg, null, 2)); } return file; }); }; - -function toBuffer(string) { - if (typeof Buffer.from === 'function') { - return Buffer.from(string, 'utf8'); - } else { - // this was deprecated in node v5 in favor - // of Buffer.from(string, encoding) - return new Buffer(string, 'utf8'); - } -} diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 7d95110b98a129..6117190c57ba89 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -329,7 +329,7 @@ describe('Response factory', () => { const router = createRouter('/'); router.get({ path: '/', validate: false }, (context, req, res) => { - const buffer = new Buffer('abc'); + const buffer = Buffer.from('abc'); return res.ok({ body: buffer, diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 6cfcaca5843b3e..e5edf5bd9c2609 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -14,6 +14,8 @@ cacheDir="$HOME/.kibana" RED='\033[0;31m' C_RESET='\033[0m' # Reset color +export NODE_OPTIONS="$NODE_OPTIONS --throw-deprecation" + ### ### Since the Jenkins logging output collector doesn't look like a TTY ### Node/Chalk and other color libs disable their color output. But Jenkins diff --git a/src/legacy/core_plugins/console/server/__tests__/proxy_config_collection.js b/src/legacy/core_plugins/console/server/__tests__/proxy_config_collection.js index 91797f897d8ba4..e575b0f707aadd 100644 --- a/src/legacy/core_plugins/console/server/__tests__/proxy_config_collection.js +++ b/src/legacy/core_plugins/console/server/__tests__/proxy_config_collection.js @@ -28,7 +28,7 @@ import { ProxyConfigCollection } from '../proxy_config_collection'; describe('ProxyConfigCollection', function () { beforeEach(function () { - sinon.stub(fs, 'readFileSync').callsFake(() => new Buffer(0)); + sinon.stub(fs, 'readFileSync').callsFake(() => Buffer.alloc(0)); }); afterEach(function () { diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/__jest__/step_time_field.test.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/__jest__/step_time_field.test.js index 5677d2293a725c..1f9d80440f6cbc 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/__jest__/step_time_field.test.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/components/step_time_field/__jest__/step_time_field.test.js @@ -43,6 +43,7 @@ const indexPatternsService = { make: async () => ({ fieldsFetcher: { fetch: noop, + fetchForWildcard: noop, }, }), }; diff --git a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts index 2b2e946198e0a8..cf6059faf0c056 100644 --- a/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts +++ b/src/legacy/core_plugins/telemetry/server/collectors/usage/telemetry_usage_collector.test.ts @@ -75,7 +75,7 @@ describe('telemetry_usage_collector', () => { // empty writeFileSync(tempFiles.empty, ''); // 1 byte too big - writeFileSync(tempFiles.too_big, new Buffer(MAX_FILE_SIZE + 1)); + writeFileSync(tempFiles.too_big, Buffer.alloc(MAX_FILE_SIZE + 1)); // write-only file writeFileSync(tempFiles.unreadable, 'valid: true', { mode: 0o222 }); // valid diff --git a/src/legacy/core_plugins/timelion/server/series_functions/__tests__/yaxis.js b/src/legacy/core_plugins/timelion/server/series_functions/__tests__/yaxis.js index ebc187100c7e18..2aa4b9a471c482 100644 --- a/src/legacy/core_plugins/timelion/server/series_functions/__tests__/yaxis.js +++ b/src/legacy/core_plugins/timelion/server/series_functions/__tests__/yaxis.js @@ -95,16 +95,16 @@ describe('yaxis.js', () => { it('throws an error if currency is not three letter code', () => { invoke(fn, [seriesList, 1, null, null, null, null, null, 'currency:abcde']).catch(e => { - expect(e).to.be.an(Error); + expect(e).to.be.an.instanceof(Error); }); invoke(fn, [seriesList, 1, null, null, null, null, null, 'currency:12']).catch(e => { - expect(e).to.be.an(Error); + expect(e).to.be.an.instanceof(Error); }); invoke(fn, [seriesList, 1, null, null, null, null, null, 'currency:$#']).catch(e => { - expect(e).to.be.an(Error); + expect(e).to.be.an.instanceof(Error); }); invoke(fn, [seriesList, 1, null, null, null, null, null, 'currency:ab']).catch(e => { - expect(e).to.be.an(Error); + expect(e).to.be.an.instanceof(Error); }); }); diff --git a/src/legacy/server/sass/build.js b/src/legacy/server/sass/build.js index db1b26e1028348..2872ef5daa2004 100644 --- a/src/legacy/server/sass/build.js +++ b/src/legacy/server/sass/build.js @@ -30,7 +30,7 @@ import { PUBLIC_PATH_PLACEHOLDER } from '../../../optimize/public_path_placehold const renderSass = promisify(sass.render); const writeFile = promisify(fs.writeFile); -const exists = promisify(fs.exists); +const access = promisify(fs.access); const copyFile = promisify(fs.copyFile); const mkdirAsync = promisify(fs.mkdir); @@ -145,23 +145,27 @@ export class Build { ]; // verify that asset sources exist and import is valid before writing anything - await Promise.all(urlAssets.map(async (asset) => { - if (!await exists(asset.path)) { - throw this._makeError( - 'Invalid url() in css output', - `url("${asset.requestUrl}") resolves to "${asset.path}", which does not exist.\n` + - ` Make sure that the request is relative to "${asset.root}"` - ); - } + await Promise.all( + urlAssets.map(async asset => { + try { + await access(asset.path); + } catch (e) { + throw this._makeError( + 'Invalid url() in css output', + `url("${asset.requestUrl}") resolves to "${asset.path}", which does not exist.\n` + + ` Make sure that the request is relative to "${asset.root}"` + ); + } - if (!isPathInside(asset.path, asset.boundry)) { - throw this._makeError( - 'Invalid url() in css output', - `url("${asset.requestUrl}") resolves to "${asset.path}"\n` + - ` which is outside of "${asset.boundry}"` - ); - } - })); + if (!isPathInside(asset.path, asset.boundry)) { + throw this._makeError( + 'Invalid url() in css output', + `url("${asset.requestUrl}") resolves to "${asset.path}"\n` + + ` which is outside of "${asset.boundry}"` + ); + } + }) + ); // write css await mkdirAsync(this.targetDir, { recursive: true }); diff --git a/src/legacy/ui/public/chrome/__mocks__/index.js b/src/legacy/ui/public/chrome/__mocks__/index.js index e8149970002a13..0d3e580d4b4f08 100644 --- a/src/legacy/ui/public/chrome/__mocks__/index.js +++ b/src/legacy/ui/public/chrome/__mocks__/index.js @@ -40,3 +40,53 @@ const chrome = { // eslint-disable-next-line import/no-default-export export default chrome; + +// Copied from `src/legacy/ui/public/chrome/chrome.js` +import _ from 'lodash'; +import angular from 'angular'; +import { metadata } from '../../metadata'; + +const internals = _.defaults( + _.cloneDeep(metadata), + { + basePath: '', + rootController: null, + rootTemplate: null, + showAppsLink: null, + xsrfToken: null, + devMode: true, + brand: null, + nav: [], + applicationClasses: [] + } +); + +const waitForBootstrap = new Promise(resolve => { + chrome.bootstrap = function (targetDomElement) { + // import chrome nav controls and hacks now so that they are executed after + // everything else, can safely import the chrome, and interact with services + // and such setup by all other modules + require('uiExports/chromeNavControls'); + require('uiExports/hacks'); + + // sets attribute on body for stylesheet sandboxing + document.body.setAttribute('id', `${internals.app.id}-app`); + + chrome.setupAngular(); + targetDomElement.setAttribute('kbn-chrome', 'true'); + targetDomElement.setAttribute('ng-class', '{ \'hidden-chrome\': !chrome.getVisible() }'); + targetDomElement.className = 'app-wrapper'; + angular.bootstrap(targetDomElement, ['kibana']); + resolve(targetDomElement); + }; +}); + +chrome.dangerouslyGetActiveInjector = () => { + return waitForBootstrap.then((targetDomElement) => { + const $injector = angular.element(targetDomElement).injector(); + if (!$injector) { + return Promise.reject('targetDomElement had no angular context after bootstrapping'); + } + return $injector; + }); +}; diff --git a/src/legacy/utils/deep_clone_with_buffers.test.ts b/src/legacy/utils/deep_clone_with_buffers.test.ts index 8fdf1ae7bfd98e..7a0906a715c2e1 100644 --- a/src/legacy/utils/deep_clone_with_buffers.test.ts +++ b/src/legacy/utils/deep_clone_with_buffers.test.ts @@ -52,7 +52,7 @@ describe('deepCloneWithBuffers()', () => { }); it('copies buffers but keeps them buffers', () => { - const input = new Buffer('i am a teapot', 'utf8'); + const input = Buffer.from('i am a teapot', 'utf8'); const output = deepCloneWithBuffers(input); expect(Buffer.isBuffer(input)).toBe(true); @@ -65,7 +65,7 @@ describe('deepCloneWithBuffers()', () => { const input = { a: { b: { - c: new Buffer('i am a teapot', 'utf8'), + c: Buffer.from('i am a teapot', 'utf8'), }, }, }; diff --git a/src/legacy/utils/deep_clone_with_buffers.ts b/src/legacy/utils/deep_clone_with_buffers.ts index 6938b9371435e7..2e9120eb32b7c8 100644 --- a/src/legacy/utils/deep_clone_with_buffers.ts +++ b/src/legacy/utils/deep_clone_with_buffers.ts @@ -24,7 +24,7 @@ import { cloneDeep } from 'lodash'; // type of the customizer function doesn't expect that. function cloneBuffersCustomizer(val: unknown): any { if (Buffer.isBuffer(val)) { - return new Buffer(val); + return Buffer.from(val); } } diff --git a/src/optimize/dynamic_dll_plugin/dll_compiler.js b/src/optimize/dynamic_dll_plugin/dll_compiler.js index 3f3bb3e4e196c7..42543655151138 100644 --- a/src/optimize/dynamic_dll_plugin/dll_compiler.js +++ b/src/optimize/dynamic_dll_plugin/dll_compiler.js @@ -29,7 +29,7 @@ import del from 'del'; const readFileAsync = promisify(fs.readFile); const mkdirAsync = promisify(fs.mkdir); -const existsAsync = promisify(fs.exists); +const accessAsync = promisify(fs.access); const writeFileAsync = promisify(fs.writeFile); export class DllCompiler { @@ -127,13 +127,14 @@ export class DllCompiler { } async ensurePathExists(filePath) { - const exists = await existsAsync(filePath); - - if (!exists) { + try { + await accessAsync(filePath); + } catch (e) { await mkdirAsync(path.dirname(filePath), { recursive: true }); + return false; } - return exists; + return true; } async ensureOutputPathExists() { diff --git a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.test.tsx b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.test.tsx index de29e1dec85a86..4438a6c9971261 100644 --- a/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.test.tsx +++ b/src/plugins/dashboard_embeddable_container/public/actions/replace_panel_action.test.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import { isErrorEmbeddable, EmbeddableFactory } from '../embeddable_plugin'; import { ReplacePanelAction } from './replace_panel_action'; import { DashboardContainer } from '../embeddable'; @@ -29,6 +28,8 @@ import { ContactCardEmbeddableOutput, } from '../embeddable_plugin_test_samples'; import { DashboardOptions } from '../embeddable/dashboard_container_factory'; +import { coreMock } from '../../../../core/public/mocks'; +import { CoreStart } from 'kibana/public'; const embeddableFactories = new Map(); embeddableFactories.set( @@ -39,8 +40,9 @@ const getEmbeddableFactories = () => embeddableFactories.values(); let container: DashboardContainer; let embeddable: ContactCardEmbeddable; - +let coreStart: CoreStart; beforeEach(async () => { + coreStart = coreMock.createStart(); const options: DashboardOptions = { ExitFullScreenButton: () => null, SavedObjectFinder: () => null, @@ -50,7 +52,7 @@ beforeEach(async () => { } as any, inspector: {} as any, notifications: {} as any, - overlays: {} as any, + overlays: coreStart.overlays, savedObjectMetaData: {} as any, uiActions: {} as any, }; @@ -80,11 +82,10 @@ beforeEach(async () => { }); test('Executes the replace panel action', async () => { - let core: any; let SavedObjectFinder: any; let notifications: any; const action = new ReplacePanelAction( - core, + coreStart, SavedObjectFinder, notifications, getEmbeddableFactories @@ -93,11 +94,10 @@ test('Executes the replace panel action', async () => { }); test('Is not compatible when embeddable is not in a dashboard container', async () => { - let core: any; let SavedObjectFinder: any; let notifications: any; const action = new ReplacePanelAction( - core, + coreStart, SavedObjectFinder, notifications, getEmbeddableFactories @@ -113,11 +113,10 @@ test('Is not compatible when embeddable is not in a dashboard container', async }); test('Execute throws an error when called with an embeddable not in a parent', async () => { - let core: any; let SavedObjectFinder: any; let notifications: any; const action = new ReplacePanelAction( - core, + coreStart, SavedObjectFinder, notifications, getEmbeddableFactories @@ -129,11 +128,10 @@ test('Execute throws an error when called with an embeddable not in a parent', a }); test('Returns title', async () => { - let core: any; let SavedObjectFinder: any; let notifications: any; const action = new ReplacePanelAction( - core, + coreStart, SavedObjectFinder, notifications, getEmbeddableFactories @@ -142,11 +140,10 @@ test('Returns title', async () => { }); test('Returns an icon', async () => { - let core: any; let SavedObjectFinder: any; let notifications: any; const action = new ReplacePanelAction( - core, + coreStart, SavedObjectFinder, notifications, getEmbeddableFactories diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.test.tsx index e4338dc89153d3..c1a3d88979f490 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/grid/dashboard_grid.test.tsx @@ -21,7 +21,7 @@ import sizeMe from 'react-sizeme'; import React from 'react'; -import { nextTick, mountWithIntl } from 'test_utils/enzyme_helpers'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { skip } from 'rxjs/operators'; import { EmbeddableFactory, GetEmbeddableFactory } from '../../embeddable_plugin'; import { DashboardGrid, DashboardGridProps } from './dashboard_grid'; @@ -65,10 +65,14 @@ function prepare(props?: Partial) { } as any, notifications: {} as any, overlays: {} as any, - inspector: {} as any, + inspector: { + isAvailable: jest.fn(), + } as any, SavedObjectFinder: () => null, ExitFullScreenButton: () => null, - uiActions: {} as any, + uiActions: { + getTriggerCompatibleActions: (() => []) as any, + } as any, }; dashboardContainer = new DashboardContainer(initialInput, options); const defaultTestProps: DashboardGridProps = { @@ -100,12 +104,11 @@ test('renders DashboardGrid', () => { ); - const panelElements = component.find('EmbeddableChildPanel'); expect(panelElements.length).toBe(2); }); -test('renders DashboardGrid with no visualizations', async () => { +test('renders DashboardGrid with no visualizations', () => { const { props, options } = prepare(); const component = mountWithIntl( @@ -114,12 +117,11 @@ test('renders DashboardGrid with no visualizations', async () => { ); props.container.updateInput({ panels: {} }); - await nextTick(); component.update(); expect(component.find('EmbeddableChildPanel').length).toBe(0); }); -test('DashboardGrid removes panel when removed from container', async () => { +test('DashboardGrid removes panel when removed from container', () => { const { props, options } = prepare(); const component = mountWithIntl( @@ -131,13 +133,12 @@ test('DashboardGrid removes panel when removed from container', async () => { const filteredPanels = { ...originalPanels }; delete filteredPanels['1']; props.container.updateInput({ panels: filteredPanels }); - await nextTick(); component.update(); const panelElements = component.find('EmbeddableChildPanel'); expect(panelElements.length).toBe(1); }); -test('DashboardGrid renders expanded panel', async () => { +test('DashboardGrid renders expanded panel', () => { const { props, options } = prepare(); const component = mountWithIntl( @@ -146,7 +147,6 @@ test('DashboardGrid renders expanded panel', async () => { ); props.container.updateInput({ expandedPanelId: '1' }); - await nextTick(); component.update(); // Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized. expect(component.find('EmbeddableChildPanel').length).toBe(2); @@ -156,7 +156,6 @@ test('DashboardGrid renders expanded panel', async () => { ).toBe('1'); props.container.updateInput({ expandedPanelId: undefined }); - await nextTick(); component.update(); expect(component.find('EmbeddableChildPanel').length).toBe(2); diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.test.tsx index 7b83407bf8063c..a2f7b8dc28fb05 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.test.tsx +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/dashboard_viewport.test.tsx @@ -56,10 +56,14 @@ function getProps( } as any, notifications: {} as any, overlays: {} as any, - inspector: {} as any, + inspector: { + isAvailable: jest.fn(), + } as any, SavedObjectFinder: () => null, ExitFullScreenButton, - uiActions: {} as any, + uiActions: { + getTriggerCompatibleActions: (() => []) as any, + } as any, }; const input = getSampleDashboardInput({ diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 61aac70b4a7ecb..4a5b0bed5e819a 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -148,6 +148,74 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "setIsCollapsed": [MockFunction], "setIsVisible": [MockFunction], }, + "data": Object { + "autocomplete": Object { + "addProvider": [MockFunction], + "clearProviders": [MockFunction], + "getProvider": [MockFunction], + }, + "fieldFormats": Object { + "getByFieldType": [MockFunction], + "getConfig": [MockFunction], + "getDefaultConfig": [MockFunction], + "getDefaultInstance": [MockFunction], + "getDefaultInstanceCacheResolver": [MockFunction], + "getDefaultInstancePlain": [MockFunction], + "getDefaultType": [MockFunction], + "getDefaultTypeName": [MockFunction], + "getInstance": [MockFunction], + "getType": [MockFunction], + "getTypeNameByEsTypes": [MockFunction], + "init": [MockFunction], + "parseDefaultTypeMap": [MockFunction], + "register": [MockFunction], + }, + "getSuggestions": [MockFunction], + "indexPatterns": Object { + "FieldList": Object {}, + "IndexPatternSelect": [MockFunction], + "flattenHitWrapper": [MockFunction], + "formatHitProvider": [MockFunction], + "indexPatterns": [MockFunction], + }, + "query": Object { + "filterManager": [MockFunction], + "savedQueries": [MockFunction], + "timefilter": Object { + "history": Object { + "add": [MockFunction], + "get": [MockFunction], + }, + "timefilter": Object { + "calculateBounds": [MockFunction], + "createFilter": [MockFunction], + "disableAutoRefreshSelector": [MockFunction], + "disableTimeRangeSelector": [MockFunction], + "enableAutoRefreshSelector": [MockFunction], + "enableTimeRangeSelector": [MockFunction], + "getActiveBounds": [MockFunction], + "getAutoRefreshFetch$": [MockFunction], + "getBounds": [MockFunction], + "getEnabledUpdated$": [MockFunction], + "getFetch$": [MockFunction], + "getRefreshInterval": [MockFunction], + "getRefreshIntervalUpdate$": [MockFunction], + "getTime": [MockFunction], + "getTimeUpdate$": [MockFunction], + "isAutoRefreshSelectorEnabled": [MockFunction], + "isTimeRangeSelectorEnabled": [MockFunction], + "setRefreshInterval": [MockFunction], + "setTime": [MockFunction], + }, + }, + }, + "search": Object { + "search": [MockFunction], + }, + "ui": Object { + "IndexPatternSelect": [MockFunction], + }, + }, "docLinks": Object { "DOC_LINK_VERSION": "mocked-test-branch", "ELASTIC_WEBSITE_URL": "https://www.elastic.co/", @@ -710,6 +778,74 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA "setIsCollapsed": [MockFunction], "setIsVisible": [MockFunction], }, + "data": Object { + "autocomplete": Object { + "addProvider": [MockFunction], + "clearProviders": [MockFunction], + "getProvider": [MockFunction], + }, + "fieldFormats": Object { + "getByFieldType": [MockFunction], + "getConfig": [MockFunction], + "getDefaultConfig": [MockFunction], + "getDefaultInstance": [MockFunction], + "getDefaultInstanceCacheResolver": [MockFunction], + "getDefaultInstancePlain": [MockFunction], + "getDefaultType": [MockFunction], + "getDefaultTypeName": [MockFunction], + "getInstance": [MockFunction], + "getType": [MockFunction], + "getTypeNameByEsTypes": [MockFunction], + "init": [MockFunction], + "parseDefaultTypeMap": [MockFunction], + "register": [MockFunction], + }, + "getSuggestions": [MockFunction], + "indexPatterns": Object { + "FieldList": Object {}, + "IndexPatternSelect": [MockFunction], + "flattenHitWrapper": [MockFunction], + "formatHitProvider": [MockFunction], + "indexPatterns": [MockFunction], + }, + "query": Object { + "filterManager": [MockFunction], + "savedQueries": [MockFunction], + "timefilter": Object { + "history": Object { + "add": [MockFunction], + "get": [MockFunction], + }, + "timefilter": Object { + "calculateBounds": [MockFunction], + "createFilter": [MockFunction], + "disableAutoRefreshSelector": [MockFunction], + "disableTimeRangeSelector": [MockFunction], + "enableAutoRefreshSelector": [MockFunction], + "enableTimeRangeSelector": [MockFunction], + "getActiveBounds": [MockFunction], + "getAutoRefreshFetch$": [MockFunction], + "getBounds": [MockFunction], + "getEnabledUpdated$": [MockFunction], + "getFetch$": [MockFunction], + "getRefreshInterval": [MockFunction], + "getRefreshIntervalUpdate$": [MockFunction], + "getTime": [MockFunction], + "getTimeUpdate$": [MockFunction], + "isAutoRefreshSelectorEnabled": [MockFunction], + "isTimeRangeSelectorEnabled": [MockFunction], + "setRefreshInterval": [MockFunction], + "setTime": [MockFunction], + }, + }, + }, + "search": Object { + "search": [MockFunction], + }, + "ui": Object { + "IndexPatternSelect": [MockFunction], + }, + }, "docLinks": Object { "DOC_LINK_VERSION": "mocked-test-branch", "ELASTIC_WEBSITE_URL": "https://www.elastic.co/", @@ -1260,6 +1396,74 @@ exports[`QueryStringInput Should pass the query language to the language switche "setIsCollapsed": [MockFunction], "setIsVisible": [MockFunction], }, + "data": Object { + "autocomplete": Object { + "addProvider": [MockFunction], + "clearProviders": [MockFunction], + "getProvider": [MockFunction], + }, + "fieldFormats": Object { + "getByFieldType": [MockFunction], + "getConfig": [MockFunction], + "getDefaultConfig": [MockFunction], + "getDefaultInstance": [MockFunction], + "getDefaultInstanceCacheResolver": [MockFunction], + "getDefaultInstancePlain": [MockFunction], + "getDefaultType": [MockFunction], + "getDefaultTypeName": [MockFunction], + "getInstance": [MockFunction], + "getType": [MockFunction], + "getTypeNameByEsTypes": [MockFunction], + "init": [MockFunction], + "parseDefaultTypeMap": [MockFunction], + "register": [MockFunction], + }, + "getSuggestions": [MockFunction], + "indexPatterns": Object { + "FieldList": Object {}, + "IndexPatternSelect": [MockFunction], + "flattenHitWrapper": [MockFunction], + "formatHitProvider": [MockFunction], + "indexPatterns": [MockFunction], + }, + "query": Object { + "filterManager": [MockFunction], + "savedQueries": [MockFunction], + "timefilter": Object { + "history": Object { + "add": [MockFunction], + "get": [MockFunction], + }, + "timefilter": Object { + "calculateBounds": [MockFunction], + "createFilter": [MockFunction], + "disableAutoRefreshSelector": [MockFunction], + "disableTimeRangeSelector": [MockFunction], + "enableAutoRefreshSelector": [MockFunction], + "enableTimeRangeSelector": [MockFunction], + "getActiveBounds": [MockFunction], + "getAutoRefreshFetch$": [MockFunction], + "getBounds": [MockFunction], + "getEnabledUpdated$": [MockFunction], + "getFetch$": [MockFunction], + "getRefreshInterval": [MockFunction], + "getRefreshIntervalUpdate$": [MockFunction], + "getTime": [MockFunction], + "getTimeUpdate$": [MockFunction], + "isAutoRefreshSelectorEnabled": [MockFunction], + "isTimeRangeSelectorEnabled": [MockFunction], + "setRefreshInterval": [MockFunction], + "setTime": [MockFunction], + }, + }, + }, + "search": Object { + "search": [MockFunction], + }, + "ui": Object { + "IndexPatternSelect": [MockFunction], + }, + }, "docLinks": Object { "DOC_LINK_VERSION": "mocked-test-branch", "ELASTIC_WEBSITE_URL": "https://www.elastic.co/", @@ -1819,6 +2023,74 @@ exports[`QueryStringInput Should pass the query language to the language switche "setIsCollapsed": [MockFunction], "setIsVisible": [MockFunction], }, + "data": Object { + "autocomplete": Object { + "addProvider": [MockFunction], + "clearProviders": [MockFunction], + "getProvider": [MockFunction], + }, + "fieldFormats": Object { + "getByFieldType": [MockFunction], + "getConfig": [MockFunction], + "getDefaultConfig": [MockFunction], + "getDefaultInstance": [MockFunction], + "getDefaultInstanceCacheResolver": [MockFunction], + "getDefaultInstancePlain": [MockFunction], + "getDefaultType": [MockFunction], + "getDefaultTypeName": [MockFunction], + "getInstance": [MockFunction], + "getType": [MockFunction], + "getTypeNameByEsTypes": [MockFunction], + "init": [MockFunction], + "parseDefaultTypeMap": [MockFunction], + "register": [MockFunction], + }, + "getSuggestions": [MockFunction], + "indexPatterns": Object { + "FieldList": Object {}, + "IndexPatternSelect": [MockFunction], + "flattenHitWrapper": [MockFunction], + "formatHitProvider": [MockFunction], + "indexPatterns": [MockFunction], + }, + "query": Object { + "filterManager": [MockFunction], + "savedQueries": [MockFunction], + "timefilter": Object { + "history": Object { + "add": [MockFunction], + "get": [MockFunction], + }, + "timefilter": Object { + "calculateBounds": [MockFunction], + "createFilter": [MockFunction], + "disableAutoRefreshSelector": [MockFunction], + "disableTimeRangeSelector": [MockFunction], + "enableAutoRefreshSelector": [MockFunction], + "enableTimeRangeSelector": [MockFunction], + "getActiveBounds": [MockFunction], + "getAutoRefreshFetch$": [MockFunction], + "getBounds": [MockFunction], + "getEnabledUpdated$": [MockFunction], + "getFetch$": [MockFunction], + "getRefreshInterval": [MockFunction], + "getRefreshIntervalUpdate$": [MockFunction], + "getTime": [MockFunction], + "getTimeUpdate$": [MockFunction], + "isAutoRefreshSelectorEnabled": [MockFunction], + "isTimeRangeSelectorEnabled": [MockFunction], + "setRefreshInterval": [MockFunction], + "setTime": [MockFunction], + }, + }, + }, + "search": Object { + "search": [MockFunction], + }, + "ui": Object { + "IndexPatternSelect": [MockFunction], + }, + }, "docLinks": Object { "DOC_LINK_VERSION": "mocked-test-branch", "ELASTIC_WEBSITE_URL": "https://www.elastic.co/", @@ -2369,6 +2641,74 @@ exports[`QueryStringInput Should render the given query 1`] = ` "setIsCollapsed": [MockFunction], "setIsVisible": [MockFunction], }, + "data": Object { + "autocomplete": Object { + "addProvider": [MockFunction], + "clearProviders": [MockFunction], + "getProvider": [MockFunction], + }, + "fieldFormats": Object { + "getByFieldType": [MockFunction], + "getConfig": [MockFunction], + "getDefaultConfig": [MockFunction], + "getDefaultInstance": [MockFunction], + "getDefaultInstanceCacheResolver": [MockFunction], + "getDefaultInstancePlain": [MockFunction], + "getDefaultType": [MockFunction], + "getDefaultTypeName": [MockFunction], + "getInstance": [MockFunction], + "getType": [MockFunction], + "getTypeNameByEsTypes": [MockFunction], + "init": [MockFunction], + "parseDefaultTypeMap": [MockFunction], + "register": [MockFunction], + }, + "getSuggestions": [MockFunction], + "indexPatterns": Object { + "FieldList": Object {}, + "IndexPatternSelect": [MockFunction], + "flattenHitWrapper": [MockFunction], + "formatHitProvider": [MockFunction], + "indexPatterns": [MockFunction], + }, + "query": Object { + "filterManager": [MockFunction], + "savedQueries": [MockFunction], + "timefilter": Object { + "history": Object { + "add": [MockFunction], + "get": [MockFunction], + }, + "timefilter": Object { + "calculateBounds": [MockFunction], + "createFilter": [MockFunction], + "disableAutoRefreshSelector": [MockFunction], + "disableTimeRangeSelector": [MockFunction], + "enableAutoRefreshSelector": [MockFunction], + "enableTimeRangeSelector": [MockFunction], + "getActiveBounds": [MockFunction], + "getAutoRefreshFetch$": [MockFunction], + "getBounds": [MockFunction], + "getEnabledUpdated$": [MockFunction], + "getFetch$": [MockFunction], + "getRefreshInterval": [MockFunction], + "getRefreshIntervalUpdate$": [MockFunction], + "getTime": [MockFunction], + "getTimeUpdate$": [MockFunction], + "isAutoRefreshSelectorEnabled": [MockFunction], + "isTimeRangeSelectorEnabled": [MockFunction], + "setRefreshInterval": [MockFunction], + "setTime": [MockFunction], + }, + }, + }, + "search": Object { + "search": [MockFunction], + }, + "ui": Object { + "IndexPatternSelect": [MockFunction], + }, + }, "docLinks": Object { "DOC_LINK_VERSION": "mocked-test-branch", "ELASTIC_WEBSITE_URL": "https://www.elastic.co/", @@ -2928,6 +3268,74 @@ exports[`QueryStringInput Should render the given query 1`] = ` "setIsCollapsed": [MockFunction], "setIsVisible": [MockFunction], }, + "data": Object { + "autocomplete": Object { + "addProvider": [MockFunction], + "clearProviders": [MockFunction], + "getProvider": [MockFunction], + }, + "fieldFormats": Object { + "getByFieldType": [MockFunction], + "getConfig": [MockFunction], + "getDefaultConfig": [MockFunction], + "getDefaultInstance": [MockFunction], + "getDefaultInstanceCacheResolver": [MockFunction], + "getDefaultInstancePlain": [MockFunction], + "getDefaultType": [MockFunction], + "getDefaultTypeName": [MockFunction], + "getInstance": [MockFunction], + "getType": [MockFunction], + "getTypeNameByEsTypes": [MockFunction], + "init": [MockFunction], + "parseDefaultTypeMap": [MockFunction], + "register": [MockFunction], + }, + "getSuggestions": [MockFunction], + "indexPatterns": Object { + "FieldList": Object {}, + "IndexPatternSelect": [MockFunction], + "flattenHitWrapper": [MockFunction], + "formatHitProvider": [MockFunction], + "indexPatterns": [MockFunction], + }, + "query": Object { + "filterManager": [MockFunction], + "savedQueries": [MockFunction], + "timefilter": Object { + "history": Object { + "add": [MockFunction], + "get": [MockFunction], + }, + "timefilter": Object { + "calculateBounds": [MockFunction], + "createFilter": [MockFunction], + "disableAutoRefreshSelector": [MockFunction], + "disableTimeRangeSelector": [MockFunction], + "enableAutoRefreshSelector": [MockFunction], + "enableTimeRangeSelector": [MockFunction], + "getActiveBounds": [MockFunction], + "getAutoRefreshFetch$": [MockFunction], + "getBounds": [MockFunction], + "getEnabledUpdated$": [MockFunction], + "getFetch$": [MockFunction], + "getRefreshInterval": [MockFunction], + "getRefreshIntervalUpdate$": [MockFunction], + "getTime": [MockFunction], + "getTimeUpdate$": [MockFunction], + "isAutoRefreshSelectorEnabled": [MockFunction], + "isTimeRangeSelectorEnabled": [MockFunction], + "setRefreshInterval": [MockFunction], + "setTime": [MockFunction], + }, + }, + }, + "search": Object { + "search": [MockFunction], + }, + "ui": Object { + "IndexPatternSelect": [MockFunction], + }, + }, "docLinks": Object { "DOC_LINK_VERSION": "mocked-test-branch", "ELASTIC_WEBSITE_URL": "https://www.elastic.co/", diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index 51f0abbd102ccd..70d0c96b4733f3 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -24,6 +24,7 @@ import { mount } from 'enzyme'; import { QueryBarTopRow } from './query_bar_top_row'; import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../mocks'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; import { stubIndexPatternWithFields } from '../../stubs'; @@ -95,6 +96,7 @@ function wrapQueryBarTopRowInContext(testProps: any) { const services = { ...startMock, + data: dataPluginMock.createStartContract(), appName: 'discover', storage: createMockStorage(), }; @@ -117,7 +119,7 @@ describe('QueryBarTopRowTopRow', () => { jest.clearAllMocks(); }); - it('Should render the given query', () => { + it('Should render query and time picker', () => { const component = mount( wrapQueryBarTopRowInContext({ query: kqlQuery, diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index 3c98ee948c7d88..4435bd87cd2d71 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -28,6 +28,7 @@ import React from 'react'; import { QueryLanguageSwitcher } from './language_switcher'; import { QueryStringInput, QueryStringInputUI } from './query_string_input'; import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../mocks'; const startMock = coreMock.createStart(); import { stubIndexPatternWithFields } from '../../stubs'; @@ -74,6 +75,7 @@ function wrapQueryStringInputInContext(testProps: any, storage?: any) { const services = { ...startMock, + data: dataPluginMock.createStartContract(), appName: testProps.appName || 'test', storage: storage || createMockStorage(), }; diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx index 5b3d638f9e9351..b35ba427378abb 100644 --- a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.test.tsx @@ -251,6 +251,7 @@ describe('SavedObjectsFinder', () => { it('should include additional fields in search if listed in meta data', async () => { const core = coreMock.createStart(); core.uiSettings.get.mockImplementation(() => 10); + (core.savedObjects.client.find as jest.Mock).mockResolvedValue({ savedObjects: [] }); const wrapper = shallow( { describe('loading state', () => { it('should display a spinner during initial loading', () => { const core = coreMock.createStart(); + (core.savedObjects.client.find as jest.Mock).mockResolvedValue({ savedObjects: [] }); const wrapper = shallow( { - imageDiff.getFullResult({ - actualImage: sessionImagePath, - expectedImage: baselineImagePath, - diffImage: diffImagePath, - shadow: true, - }, cb); - }); + const sessionImage = PNG.sync.read(await fs.promises.readFile(sessionImagePath)); + const baselineImage = PNG.sync.read(await fs.promises.readFile(baselineImagePath)); + const { width, height } = sessionImage; + const diff = new PNG({ width, height }); + + const numDiffPixels = pixelmatch( + sessionImage.data, + baselineImage.data, + diff.data, + width, + height, + { threshold: 0 } + ); + + await fs.promises.writeFile(diffImagePath, PNG.sync.write(diff)); - const change = diffResult.percentage; + const change = numDiffPixels / (width * height); const changePercentage = (change * 100).toFixed(2); console.log(`(${changePercentage}%) ${screenshot}`); comparison.percentage = changePercentage; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts index f05d343ad7ba5b..de423e967b0b3c 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/__test__/createErrorGroupWatch.test.ts @@ -27,7 +27,7 @@ describe('createErrorGroupWatch', () => { .mockResolvedValue(undefined); beforeEach(async () => { - jest.spyOn(uuid, 'v4').mockReturnValue(new Buffer('mocked-uuid')); + jest.spyOn(uuid, 'v4').mockReturnValue(Buffer.from('mocked-uuid')); createWatchResponse = await createErrorGroupWatch({ http: {} as HttpServiceBase, diff --git a/x-pack/legacy/plugins/index_management/__mocks__/ui/notify.js b/x-pack/legacy/plugins/index_management/__mocks__/ui/notify.js index 27d24efb79c9e6..d508c3383d5f91 100644 --- a/x-pack/legacy/plugins/index_management/__mocks__/ui/notify.js +++ b/x-pack/legacy/plugins/index_management/__mocks__/ui/notify.js @@ -10,3 +10,5 @@ export const toastNotifications = { addWarning: () => {}, addError: () => {}, }; + +export function fatalError() {} diff --git a/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx b/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx index 52dd1071deaa44..77869c40d35778 100644 --- a/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx +++ b/x-pack/legacy/plugins/reporting/public/components/report_info_button.tsx @@ -266,7 +266,6 @@ export class ReportInfoButton extends Component { info: null, error: kfetchError, }); - throw kfetchError; } } }; diff --git a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js index b2e87482b73a1e..f2a405d51fbf5b 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js +++ b/x-pack/legacy/plugins/reporting/server/lib/esqueue/__tests__/worker.js @@ -246,7 +246,9 @@ describe('Worker class', function () { it('should use error multiplier when processPendingJobs rejects the Promise', async function () { worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); - const processPendingJobsStub = sinon.stub(worker, '_processPendingJobs').returns(Promise.reject(new Error('test error'))); + const processPendingJobsStub = sinon + .stub(worker, '_processPendingJobs') + .rejects(new Error('test error')); await allowPoll(defaultWorkerOptions.interval); expect(processPendingJobsStub.callCount).to.be(1); @@ -517,7 +519,7 @@ describe('Worker class', function () { it('should emit for errors from claiming job', function (done) { sinon.stub(mockQueue.client, 'callWithInternalUser') .withArgs('update') - .returns(Promise.reject({ statusCode: 401 })); + .rejects({ statusCode: 401 }); worker.once(constants.EVENT_WORKER_JOB_CLAIM_ERROR, function (err) { try { @@ -531,13 +533,13 @@ describe('Worker class', function () { } }); - worker._claimPendingJobs(getMockJobs()); + worker._claimPendingJobs(getMockJobs()).catch(() => {}); }); it('should reject the promise if an error claiming the job', function () { sinon.stub(mockQueue.client, 'callWithInternalUser') .withArgs('update') - .returns(Promise.reject({ statusCode: 409 })); + .rejects({ statusCode: 409 }); return worker._claimPendingJobs(getMockJobs()) .catch(err => { expect(err).to.eql({ statusCode: 409 }); @@ -547,7 +549,7 @@ describe('Worker class', function () { it('should get the pending job', function () { sinon.stub(mockQueue.client, 'callWithInternalUser') .withArgs('update') - .returns(Promise.resolve({ test: 'cool' })); + .resolves({ test: 'cool' }); sinon.stub(worker, '_performJob').callsFake(identity); return worker._claimPendingJobs(getMockJobs()) .then(claimedJob => { @@ -607,7 +609,7 @@ describe('Worker class', function () { mockQueue.client.callWithInternalUser.restore(); sinon.stub(mockQueue.client, 'callWithInternalUser') .withArgs('update') - .returns(Promise.reject({ statusCode: 409 })); + .rejects({ statusCode: 409 }); return worker._failJob(job) .then((res) => expect(res).to.equal(true)); }); @@ -616,7 +618,7 @@ describe('Worker class', function () { mockQueue.client.callWithInternalUser.restore(); sinon.stub(mockQueue.client, 'callWithInternalUser') .withArgs('update') - .returns(Promise.reject({ statusCode: 401 })); + .rejects({ statusCode: 401 }); return worker._failJob(job) .then((res) => expect(res).to.equal(false)); }); @@ -654,7 +656,7 @@ describe('Worker class', function () { mockQueue.client.callWithInternalUser.restore(); sinon.stub(mockQueue.client, 'callWithInternalUser') .withArgs('update') - .returns(Promise.reject({ statusCode: 401 })); + .rejects({ statusCode: 401 }); worker.on(constants.EVENT_WORKER_FAIL_UPDATE_ERROR, function (err) { try { @@ -842,7 +844,7 @@ describe('Worker class', function () { describe('job failures', function () { function getFailStub(workerWithFailure) { - return sinon.stub(workerWithFailure, '_failJob').returns(Promise.resolve()); + return sinon.stub(workerWithFailure, '_failJob').resolves(); } describe('saving output failure', () => { @@ -857,7 +859,7 @@ describe('Worker class', function () { sinon.stub(mockQueue.client, 'callWithInternalUser') .withArgs('update') - .returns(Promise.reject({ statusCode: 413 })); + .rejects({ statusCode: 413 }); const workerFn = function (jobPayload) { return new Promise(function (resolve) { @@ -878,7 +880,7 @@ describe('Worker class', function () { it('causes _processPendingJobs to reject the Promise', function () { sinon.stub(mockQueue.client, 'callWithInternalUser') .withArgs('search') - .returns(Promise.reject(new Error('test error'))); + .rejects(new Error('test error')); worker = new Worker(mockQueue, 'test', noop, defaultWorkerOptions); return worker._processPendingJobs() .then(() => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privileges.test.tsx.snap b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privileges.test.tsx.snap index 943c3e1518cab7..1c4e553daa9692 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privileges.test.tsx.snap +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/index_privileges.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`it renders without crashing 1`] = `null`; +exports[`it renders without crashing 1`] = ``; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.test.tsx index fbfb9460ecaa7b..783d2f9893b4cb 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.test.tsx @@ -10,31 +10,36 @@ import { RoleValidator } from '../../../lib/validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; import { IndexPrivileges } from './index_privileges'; -test('it renders without crashing', () => { +// the IndexPrivileges post-mount hook kicks off some promises; +// we need to wait for those promises to resolve to ensure any errors are properly caught +const flushPromises = () => new Promise(setImmediate); + +test('it renders without crashing', async () => { const props = { role: { name: '', + kibana: [], elasticsearch: { cluster: [], indices: [], run_as: [], }, - kibana: [], }, httpClient: jest.fn(), onChange: jest.fn(), indexPatterns: [], + editable: true, allowDocumentLevelSecurity: true, allowFieldLevelSecurity: true, - editable: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], }; const wrapper = shallowWithIntl(); + await flushPromises(); expect(wrapper).toMatchSnapshot(); }); -test('it renders a IndexPrivilegeForm for each privilege on the role', () => { +test('it renders a IndexPrivilegeForm for each privilege on the role', async () => { const props = { role: { name: '', @@ -64,5 +69,6 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', () => { availableIndexPrivileges: ['all', 'read', 'write', 'index'], }; const wrapper = mountWithIntl(); + await flushPromises(); expect(wrapper.find(IndexPrivilegeForm)).toHaveLength(1); }); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx index 1f54a5aacf948d..f09084ad2bb382 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import _ from 'lodash'; -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { Role, RoleIndexPrivilege } from '../../../../../../../common/model'; import { isReadOnlyRole, isRoleEnabled } from '../../../../../../lib/role_utils'; import { getFields } from '../../../../../../objects'; @@ -74,7 +74,7 @@ export class IndexPrivileges extends Component { /> )); - return forms; + return {forms}; } public addIndexPrivilege = () => { diff --git a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js index 7a11497c10718b..86d7374a8f0d12 100644 --- a/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js +++ b/x-pack/legacy/plugins/xpack_main/server/telemetry_collection/__tests__/get_xpack.js @@ -23,7 +23,12 @@ function mockGetXPackLicense(callCluster, license, req) { local: 'true' } }) - .returns(license.then(response => ({ license: response }))); + .returns( + license.then( + response => ({ license: response }), + () => {} // Catch error so that we don't emit UnhandledPromiseRejectionWarning for tests with invalid license + ) + ); callCluster.withArgs('transport.request', { method: 'GET', @@ -33,7 +38,12 @@ function mockGetXPackLicense(callCluster, license, req) { } }) // conveniently wraps the passed in license object as { license: response }, like it really is - .returns(license.then(response => ({ license: response }))); + .returns( + license.then( + response => ({ license: response }), + () => {} // Catch error so that we don't emit UnhandledPromiseRejectionWarning for tests with invalid license + ) + ); } function mockGetXPackUsage(callCluster, usage, req) { diff --git a/x-pack/package.json b/x-pack/package.json index 9faf2f0983b707..2dc78be0f834fc 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -151,7 +151,7 @@ "null-loader": "^3.0.0", "pdf-image": "2.0.0", "pdfjs-dist": "^2.0.943", - "pixelmatch": "4.0.2", + "pixelmatch": "^5.1.0", "proxyquire": "1.8.0", "react-docgen-typescript-loader": "^3.1.1", "react-test-renderer": "^16.12.0", diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index 1ba98d58a3a5f7..a81246c8f78b04 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -515,16 +515,19 @@ describe('Authenticator', () => { expect(mockSessionStorage.clear).not.toHaveBeenCalled(); }); - it('only updates the session lifespan expiration if it does not match the current server config.', async () => { - const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest(); + describe('conditionally updates the session lifespan expiration', () => { const hr = 1000 * 60 * 60; + const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); async function createAndUpdateSession( lifespan: number | null, oldExpiration: number | null, newExpiration: number | null ) { + const user = mockAuthenticatedUser(); + const request = httpServerMock.createKibanaRequest(); + jest.spyOn(Date, 'now').mockImplementation(() => currentDate); + mockOptions = getMockOptions({ session: { idleTimeout: null, @@ -536,7 +539,7 @@ describe('Authenticator', () => { mockSessionStorage = sessionStorageMock.create(); mockSessionStorage.get.mockResolvedValue({ ...mockSessVal, - idleTimeoutExpiration: 1, + idleTimeoutExpiration: null, lifespanExpiration: oldExpiration, }); mockOptions.sessionStorageFactory.asScoped.mockReturnValue(mockSessionStorage); @@ -554,17 +557,24 @@ describe('Authenticator', () => { expect(mockSessionStorage.set).toHaveBeenCalledTimes(1); expect(mockSessionStorage.set).toHaveBeenCalledWith({ ...mockSessVal, - idleTimeoutExpiration: 1, + idleTimeoutExpiration: null, lifespanExpiration: newExpiration, }); expect(mockSessionStorage.clear).not.toHaveBeenCalled(); } - // do not change max expiration - createAndUpdateSession(hr * 8, 1234, 1234); - createAndUpdateSession(null, null, null); - // change max expiration - createAndUpdateSession(null, 1234, null); - createAndUpdateSession(hr * 8, null, hr * 8); + + it('does not change a non-null lifespan expiration when configured to non-null value.', async () => { + await createAndUpdateSession(hr * 8, 1234, 1234); + }); + it('does not change a null lifespan expiration when configured to null value.', async () => { + await createAndUpdateSession(null, null, null); + }); + it('does change a non-null lifespan expiration when configured to null value.', async () => { + await createAndUpdateSession(null, 1234, null); + }); + it('does change a null lifespan expiration when configured to non-null value', async () => { + await createAndUpdateSession(hr * 8, null, currentDate + hr * 8); + }); }); it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => { diff --git a/yarn.lock b/yarn.lock index 76bc070e27842d..69ab184d480e98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5513,21 +5513,11 @@ array-map@~0.0.0: resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" integrity sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI= -array-parallel@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/array-parallel/-/array-parallel-0.1.3.tgz#8f785308926ed5aa478c47e64d1b334b6c0c947d" - integrity sha1-j3hTCJJu1apHjEfmTRszS2wMlH0= - array-reduce@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys= -array-series@~0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/array-series/-/array-series-0.1.5.tgz#df5d37bfc5c2ef0755e2aa4f92feae7d4b5a972f" - integrity sha1-3103v8XC7wdV4qpPkv6ufUtaly8= - array-slice@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" @@ -5803,11 +5793,6 @@ async@^2.6.3: dependencies: lodash "^4.17.14" -async@~0.2.9: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E= - async@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" @@ -7133,15 +7118,6 @@ buffer@^5.1.0, buffer@^5.2.0: base64-js "^1.0.2" ieee754 "^1.1.4" -buffered-spawn@~1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffered-spawn/-/buffered-spawn-1.1.2.tgz#21ad9735dfbf6576745be0d74a23ef257bf3c58d" - integrity sha1-Ia2XNd+/ZXZ0W+DXSiPvJXvzxY0= - dependencies: - cross-spawn-async "^1.0.1" - err-code "^0.1.0" - q "^1.0.1" - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -8378,13 +8354,6 @@ commander@~2.8.1: dependencies: graceful-readlink ">= 1.0.0" -commander@~2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - integrity sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q= - dependencies: - graceful-readlink ">= 1.0.0" - common-tags@1.8.0, common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -8978,14 +8947,6 @@ cross-fetch@2.2.2: node-fetch "2.1.2" whatwg-fetch "2.0.4" -cross-spawn-async@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-1.0.1.tgz#bb525c1e420d9942552e04791a3eb2d9887a105f" - integrity sha1-u1JcHkINmUJVLgR5Gj6y2Yh6EF8= - dependencies: - lru-cache "^2.6.5" - which "^1.1.1" - cross-spawn-async@^2.1.1: version "2.2.5" resolved "https://registry.yarnpkg.com/cross-spawn-async/-/cross-spawn-async-2.2.5.tgz#845ff0c0834a3ded9d160daca6d390906bb288cc" @@ -9722,7 +9683,7 @@ debug-fabulous@1.X: memoizee "0.4.X" object-assign "4.X" -debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9, debug@~2.2.0: +debug@2.6.9, debug@^2.0.0, debug@^2.1.0, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -11011,11 +10972,6 @@ enzyme@^3.10.0: rst-selector-parser "^2.2.3" string.prototype.trim "^1.1.2" -err-code@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-0.1.2.tgz#122a92b3342b9899da02b5ac994d30f95d4763ee" - integrity sha1-EiqSszQrmJnaArWsmU0w+V1HY+4= - errlop@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/errlop/-/errlop-1.1.2.tgz#a99a48f37aa264d614e342ffdbbaa49eec9220e0" @@ -13852,15 +13808,6 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" -gm@~1.21.1: - version "1.21.1" - resolved "https://registry.yarnpkg.com/gm/-/gm-1.21.1.tgz#7ed5ed05db36d30c1943f39c3bc1c839b8f2361d" - integrity sha1-ftXtBds20wwZQ/OcO8HIObjyNh0= - dependencies: - array-parallel "~0.1.3" - array-series "~0.1.5" - debug "~2.2.0" - gonzales-pe-sl@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/gonzales-pe-sl/-/gonzales-pe-sl-4.2.3.tgz#6a868bc380645f141feeb042c6f97fcc71b59fe6" @@ -15324,18 +15271,6 @@ ignore@^5.1.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.2.tgz#e28e584d43ad7e92f96995019cc43b9e1ac49558" integrity sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ== -image-diff@1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/image-diff/-/image-diff-1.6.3.tgz#818a0e656ae89480e802e7ef14db460826f730fc" - integrity sha1-gYoOZWrolIDoAufvFNtGCCb3MPw= - dependencies: - async "~0.2.9" - buffered-spawn "~1.1.1" - commander "~2.9.0" - gm "~1.21.1" - mkdirp "~0.3.5" - tmp "0.0.23" - image-size@~0.5.0: version "0.5.5" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" @@ -18749,11 +18684,6 @@ lru-cache@4.1.x, lru-cache@^4.0.0: pseudomap "^1.0.2" yallist "^2.1.2" -lru-cache@^2.6.5: - version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" - integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -19588,7 +19518,7 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.3.5, mkdirp@^0.3.5, mkdirp@~0.3.5: +mkdirp@0.3.5, mkdirp@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= @@ -21820,13 +21750,20 @@ pirates@^4.0.0, pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" -pixelmatch@4.0.2, pixelmatch@^4.0.2: +pixelmatch@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-4.0.2.tgz#8f47dcec5011b477b67db03c243bc1f3085e8854" integrity sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ= dependencies: pngjs "^3.0.0" +pixelmatch@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.1.0.tgz#b640f0e5a03a09f235a4b818ef3b9b98d9d0b911" + integrity sha512-HqtgvuWN12tBzKJf7jYsc38Ha28Q2NYpmBL9WostEGgDHJqbTLkjydZXL1ZHM02ZnB+Dkwlxo87HBY38kMiD6A== + dependencies: + pngjs "^3.4.0" + pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" @@ -22628,7 +22565,7 @@ puppeteer@^1.13.0: rimraf "^2.6.1" ws "^6.1.0" -q@^1.0.1, q@^1.1.2: +q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= @@ -27472,11 +27409,6 @@ titleize@^1.0.1: resolved "https://registry.yarnpkg.com/titleize/-/titleize-1.0.1.tgz#21bc24fcca658eadc6d3bd3c38f2bd173769b4c5" integrity sha512-rUwGDruKq1gX+FFHbTl5qjI7teVO7eOe+C8IcQ7QT+1BK3eEUXJqbZcBOeaRP4FwSC/C1A5jDoIVta0nIQ9yew== -tmp@0.0.23: - version "0.0.23" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.23.tgz#de874aa5e974a85f0a32cdfdbd74663cb3bd9c74" - integrity sha1-3odKpel0qF8KMs39vXRmPLO9nHQ= - tmp@0.0.30: version "0.0.30" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" From ffdd39222298d7d859e21665e856e8c6b20bb351 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 4 Dec 2019 07:19:39 -0800 Subject: [PATCH 12/30] Silence Axe's complaints about missing labels for Console's textarea elements, in order to allow our automated a11y tests to pass. (#52150) --- .../editor/legacy/console_editor/editor.tsx | 20 +++++++++++++------ .../legacy/console_editor/editor_output.tsx | 9 ++++++++- .../console/public/quarantined/_app.scss | 1 + 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx index 0fa0ec732c7705..b99249b2b0016b 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -193,12 +193,20 @@ function EditorUI({ previousStateLocation = 'stored' }: EditorProps) { /> -
+ + {/* Axe complains about Ace's textarea element missing a label, which interferes with our + automated a11y tests per #52136. This wrapper does nothing to address a11y but it does + satisfy Axe. */} + + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} +
); diff --git a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx index c167155bd18a91..3690ea61d56842 100644 --- a/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ b/src/legacy/core_plugins/console/np_ready/public/application/containers/editor/legacy/console_editor/editor_output.tsx @@ -87,7 +87,14 @@ function EditorOutputUI() { return (
-
+ {/* Axe complains about Ace's textarea element missing a label, which interferes with our + automated a11y tests per #52136. This wrapper does nothing to address a11y but it does + satisfy Axe. */} + + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} +