From 2b53c74cd9c1889b84a817b7a2817a839a9b5adf Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Thu, 30 Jan 2020 19:03:14 +0000 Subject: [PATCH 01/62] chore(NA): delete data/optimize with kbn clean (#55890) Co-authored-by: Elastic Machine --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9707d3863d295d..425527a058e862 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "clean": { "extraPatterns": [ "build", - "optimize", + "data/optimize", "built_assets", ".eslintcache", ".node_binaries" From 8398a1c77cde0e190126ff13804d182fda96e74d Mon Sep 17 00:00:00 2001 From: Dan Panzarella Date: Thu, 30 Jan 2020 14:25:56 -0500 Subject: [PATCH 02/62] [Endpoint] Add Endpoint Details route (#55746) * Add Endpoint Details route * add Endpoint Details tests * sacrifices to the Type gods * update to latest endpoint schema Co-authored-by: Elastic Machine --- .../endpoint/server/routes/endpoints.test.ts | 78 +++++++++++++++++++ .../endpoint/server/routes/endpoints.ts | 32 +++++++- .../endpoint/endpoint_query_builders.test.ts | 32 +++++++- .../endpoint/endpoint_query_builders.ts | 24 ++++++ 4 files changed, 162 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts index 04a38972401ed9..be14554f128c3c 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.test.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.test.ts @@ -120,4 +120,82 @@ describe('test endpoint route', () => { expect(endpointResultList.request_page_index).toEqual(10); expect(endpointResultList.request_page_size).toEqual(10); }); + + describe('Endpoint Details route', () => { + it('should return 404 on no results', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => + Promise.resolve({ + took: 3, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 9, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + }) + ); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/endpoints') + )!; + + await routeHandler( + ({ + core: { + elasticsearch: { + dataClient: mockScopedClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.notFound).toBeCalled(); + const message = mockResponse.notFound.mock.calls[0][0]?.body; + expect(message).toEqual('Endpoint Not Found'); + }); + + it('should return a single endpoint', async () => { + const mockRequest = httpServerMock.createKibanaRequest({ + params: { id: (data as any).hits.hits[0]._id }, + }); + const response: SearchResponse = (data as unknown) as SearchResponse< + EndpointMetadata + >; + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/endpoints') + )!; + + await routeHandler( + ({ + core: { + elasticsearch: { + dataClient: mockScopedClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.ok).toBeCalled(); + const result = mockResponse.ok.mock.calls[0][0]?.body as EndpointMetadata; + expect(result).toHaveProperty('endpoint'); + }); + }); }); diff --git a/x-pack/plugins/endpoint/server/routes/endpoints.ts b/x-pack/plugins/endpoint/server/routes/endpoints.ts index 4fc3e653f94265..24ad8e3941f5e1 100644 --- a/x-pack/plugins/endpoint/server/routes/endpoints.ts +++ b/x-pack/plugins/endpoint/server/routes/endpoints.ts @@ -8,7 +8,10 @@ import { IRouter } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; -import { kibanaRequestToEndpointListQuery } from '../services/endpoint/endpoint_query_builders'; +import { + kibanaRequestToEndpointListQuery, + kibanaRequestToEndpointFetchQuery, +} from '../services/endpoint/endpoint_query_builders'; import { EndpointMetadata, EndpointResultList } from '../../common/types'; import { EndpointAppContext } from '../types'; @@ -51,6 +54,33 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp } } ); + + router.get( + { + path: '/api/endpoint/endpoints/{id}', + validate: { + params: schema.object({ id: schema.string() }), + }, + options: { authRequired: true }, + }, + async (context, req, res) => { + try { + const query = kibanaRequestToEndpointFetchQuery(req, endpointAppContext); + const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( + 'search', + query + )) as SearchResponse; + + if (response.hits.hits.length === 0) { + return res.notFound({ body: 'Endpoint Not Found' }); + } + + return res.ok({ body: response.hits.hits[0]._source }); + } catch (err) { + return res.internalError({ body: err }); + } + } + ); } function mapToEndpointResultList( diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts index 3c931a251d6977..e453f777fbd502 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.test.ts @@ -5,10 +5,13 @@ */ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; -import { kibanaRequestToEndpointListQuery } from './endpoint_query_builders'; +import { + kibanaRequestToEndpointListQuery, + kibanaRequestToEndpointFetchQuery, +} from './endpoint_query_builders'; -describe('test query builder', () => { - describe('test query builder request processing', () => { +describe('query builder', () => { + describe('EndpointListQuery', () => { it('test default query params for all endpoints when no params or body is provided', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: {}, @@ -51,4 +54,27 @@ describe('test query builder', () => { } as Record); }); }); + + describe('EndpointFetchQuery', () => { + it('searches for the correct ID', () => { + const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; + const mockRequest = httpServerMock.createKibanaRequest({ + params: { + id: mockID, + }, + }); + const query = kibanaRequestToEndpointFetchQuery(mockRequest, { + logFactory: loggingServiceMock.create(), + config: () => Promise.resolve(EndpointConfigSchema.validate({})), + }); + expect(query).toEqual({ + body: { + query: { match: { 'host.id.keyword': mockID } }, + sort: [{ 'event.created': { order: 'desc' } }], + size: 1, + }, + index: 'endpoint-agent*', + }); + }); + }); }); diff --git a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts index 102c268cf9ec4c..b4f295a64b6ea0 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts +++ b/x-pack/plugins/endpoint/server/services/endpoint/endpoint_query_builders.ts @@ -65,3 +65,27 @@ async function getPagingProperties( pageIndex: pagingProperties.page_index || config.endpointResultListDefaultFirstPageIndex, }; } + +export const kibanaRequestToEndpointFetchQuery = ( + request: KibanaRequest, + endpointAppContext: EndpointAppContext +) => { + return { + body: { + query: { + match: { + 'host.id.keyword': request.params.id, + }, + }, + sort: [ + { + 'event.created': { + order: 'desc', + }, + }, + ], + size: 1, + }, + index: EndpointAppConstants.ENDPOINT_INDEX_NAME, + }; +}; From 642c694117ac3dcea33c6597a33b94cb276a75d4 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Thu, 30 Jan 2020 13:55:17 -0600 Subject: [PATCH 03/62] Add missing docker settings (#56411) Closes #54811 --- .../resources/bin/kibana-docker | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 7a82ca0b1609c0..34ba25f92beb6a 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -18,6 +18,11 @@ kibana_vars=( console.enabled console.proxyConfig console.proxyFilter + cpu.cgroup.path.override + cpuacct.cgroup.path.override + csp.rules + csp.strict + csp.warnLegacyBrowsers elasticsearch.customHeaders elasticsearch.hosts elasticsearch.logQueries @@ -30,6 +35,7 @@ kibana_vars=( elasticsearch.sniffInterval elasticsearch.sniffOnConnectionFault elasticsearch.sniffOnStart + elasticsearch.ssl.alwaysPresentCertificate elasticsearch.ssl.certificate elasticsearch.ssl.certificateAuthorities elasticsearch.ssl.key @@ -42,9 +48,13 @@ kibana_vars=( elasticsearch.startupTimeout elasticsearch.username i18n.locale + interpreter.enableInVisualize + kibana.autocompleteTerminateAfter + kibana.autocompleteTimeout kibana.defaultAppId kibana.index logging.dest + logging.json logging.quiet logging.rotate.enabled logging.rotate.everyBytes @@ -55,18 +65,32 @@ kibana_vars=( logging.useUTC logging.verbose map.includeElasticMapsService + map.proxyElasticMapsServiceInMaps + map.regionmap + map.tilemap.options.attribution + map.tilemap.options.maxZoom + map.tilemap.options.minZoom + map.tilemap.options.subdomains + map.tilemap.url + newsfeed.enabled ops.interval path.data pid.file regionmap server.basePath server.customResponseHeaders + server.compression.enabled + server.compression.referrerWhitelist + server.cors + server.cors.origin server.defaultRoute server.host + server.keepAliveTimeout server.maxPayloadBytes server.name server.port server.rewriteBasePath + server.socketTimeout server.ssl.cert server.ssl.certificate server.ssl.certificateAuthorities @@ -82,6 +106,7 @@ kibana_vars=( server.ssl.truststore.password server.ssl.redirectHttpFromPort server.ssl.supportedProtocols + server.xsrf.disableProtection server.xsrf.whitelist status.allowAnonymous status.v6ApiFormat @@ -96,11 +121,13 @@ kibana_vars=( xpack.apm.serviceMapEnabled xpack.apm.ui.enabled xpack.apm.ui.maxTraceItems + xpack.apm.ui.transactionGroupBucketSize apm_oss.apmAgentConfigurationIndex apm_oss.indexPattern apm_oss.errorIndices apm_oss.onboardingIndices apm_oss.spanIndices + apm_oss.sourcemapIndices apm_oss.transactionIndices apm_oss.metricsIndices xpack.canvas.enabled @@ -116,6 +143,8 @@ kibana_vars=( xpack.code.security.gitHostWhitelist xpack.code.security.gitProtocolWhitelist xpack.graph.enabled + xpack.graph.canEditDrillDownUrls + xpack.graph.savePolicy xpack.grokdebugger.enabled xpack.infra.enabled xpack.infra.query.partitionFactor @@ -128,11 +157,14 @@ kibana_vars=( xpack.infra.sources.default.fields.timestamp xpack.infra.sources.default.logAlias xpack.infra.sources.default.metricAlias + xpack.license_management.enabled xpack.ml.enabled + xpack.monitoring.cluster_alerts.email_notifications.email_address xpack.monitoring.elasticsearch.password xpack.monitoring.elasticsearch.pingTimeout xpack.monitoring.elasticsearch.hosts xpack.monitoring.elasticsearch.username + xpack.monitoring.elasticsearch.logFetchCount xpack.monitoring.elasticsearch.ssl.certificateAuthorities xpack.monitoring.elasticsearch.ssl.verificationMode xpack.monitoring.enabled @@ -166,6 +198,7 @@ kibana_vars=( xpack.reporting.csv.maxSizeBytes xpack.reporting.csv.scroll.duration xpack.reporting.csv.scroll.size + xpack.reporting.capture.maxAttempts xpack.reporting.enabled xpack.reporting.encryptionKey xpack.reporting.index @@ -183,6 +216,8 @@ kibana_vars=( xpack.reporting.queue.pollIntervalErrorMultiplier xpack.reporting.queue.timeout xpack.reporting.roles.allow + xpack.rollup.enabled + xpack.security.audit.enabled xpack.searchprofiler.enabled xpack.security.authc.providers xpack.security.authc.oidc.realm @@ -196,7 +231,10 @@ kibana_vars=( xpack.security.session.idleTimeout xpack.security.session.lifespan xpack.security.loginAssistanceMessage + telemetry.allowChangingOptInStatus telemetry.enabled + telemetry.optIn + telemetry.optInStatusUrl telemetry.sendUsageFrom ) From 89b3c428c21dfd6fb91d9245c7b095ebcffa3341 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 30 Jan 2020 21:03:03 +0100 Subject: [PATCH 04/62] [ML] Fix Data Visualizer responsive layout (#56372) * [ML] replace Example component with EuiListGroup * [ML] center alignment --- .../field_data_card/examples_list/example.tsx | 31 ------------------- .../examples_list/examples_list.tsx | 17 +++++++--- 2 files changed, 12 insertions(+), 36 deletions(-) delete mode 100644 x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx deleted file mode 100644 index 29fe690f4a43bb..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/example.tsx +++ /dev/null @@ -1,31 +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 React, { FC } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; - -interface Props { - example: string | object; -} - -export const Example: FC = ({ example }) => { - const exampleStr = typeof example === 'string' ? example : JSON.stringify(example); - - // Use 95% width for each example so that the truncation ellipses show up when - // wrapped inside a tooltip. - return ( - - - - - {exampleStr} - - - - - ); -}; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx index 0bf911c1edf862..c8eb8101154017 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/examples_list/examples_list.tsx @@ -6,12 +6,10 @@ import React, { FC } from 'react'; -import { EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiListGroup, EuiListGroupItem, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Example } from './example'; - interface Props { examples: Array; } @@ -22,7 +20,14 @@ export const ExamplesList: FC = ({ examples }) => { } const examplesContent = examples.map((example, i) => { - return ; + return ( + + ); }); return ( @@ -39,7 +44,9 @@ export const ExamplesList: FC = ({ examples }) => { - {examplesContent} + + {examplesContent} + ); }; From 08e0cbc3ee90358148eafb2d14a2368004b76f15 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Thu, 30 Jan 2020 13:05:09 -0700 Subject: [PATCH 05/62] [skip-ci] Add example for migrating pre-handlers (#56080) --- src/core/MIGRATION_EXAMPLES.md | 137 +++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/src/core/MIGRATION_EXAMPLES.md b/src/core/MIGRATION_EXAMPLES.md index 568980f50117df..5517dfa7f9a23c 100644 --- a/src/core/MIGRATION_EXAMPLES.md +++ b/src/core/MIGRATION_EXAMPLES.md @@ -14,6 +14,7 @@ APIs to their New Platform equivalents. - [3. New Platform shim using New Platform router](#3-new-platform-shim-using-new-platform-router) - [4. New Platform plugin](#4-new-platform-plugin) - [Accessing Services](#accessing-services) + - [Migrating Hapi "pre" handlers](#migrating-hapi-pre-handlers) - [Chrome](#chrome) - [Updating an application navlink](#updating-application-navlink) - [Chromeless Applications](#chromeless-applications) @@ -450,6 +451,142 @@ class Plugin { } ``` +### Migrating Hapi "pre" handlers + +In the Legacy Platform, routes could provide a "pre" option in their config to +register a function that should be run prior to the route handler. These +"pre" handlers allow routes to share some business logic that may do some +pre-work or validation. In Kibana, these are often used for license checks. + +The Kibana Platform's HTTP interface does not provide this functionality, +however it is simple enough to port over using a higher-order function that can +wrap the route handler. + +#### Simple example + +In this simple example, a pre-handler is used to either abort the request with +an error or continue as normal. This is a simple "gate-keeping" pattern. + +```ts +// Legacy pre-handler +const licensePreRouting = (request) => { + const licenseInfo = getMyPluginLicenseInfo(request.server.plugins.xpack_main); + if (!licenseInfo.isOneOf(['gold', 'platinum', 'trial'])) { + throw Boom.forbidden(`You don't have the right license for MyPlugin!`); + } +} + +server.route({ + method: 'GET', + path: '/api/my-plugin/do-something', + config: { + pre: [{ method: licensePreRouting }] + }, + handler: (req) => { + return doSomethingInteresting(); + } +}) +``` + +In the Kibana Platform, the same functionality can be acheived by creating a +function that takes a route handler (or factory for a route handler) as an +argument and either invokes it in the successful case or returns an error +response in the failure case. + +We'll call this a "high-order handler" similar to the "high-order component" +pattern common in the React ecosystem. + +```ts +// New Platform high-order handler +const checkLicense = ( + handler: RequestHandler +): RequestHandler => { + return (context, req, res) => { + const licenseInfo = getMyPluginLicenseInfo(context.licensing.license); + + if (licenseInfo.hasAtLeast('gold')) { + return handler(context, req, res); + } else { + return res.forbidden({ body: `You don't have the right license for MyPlugin!` }); + } + } +} + +router.get( + { path: '/api/my-plugin/do-something', validate: false }, + checkLicense(async (context, req, res) => { + const results = doSomethingInteresting(); + return res.ok({ body: results }); + }), +) +``` + +#### Full Example + +In some cases, the route handler may need access to data that the pre-handler +retrieves. In this case, you can utilize a handler _factory_ rather than a raw +handler. + +```ts +// Legacy pre-handler +const licensePreRouting = (request) => { + const licenseInfo = getMyPluginLicenseInfo(request.server.plugins.xpack_main); + if (licenseInfo.isOneOf(['gold', 'platinum', 'trial'])) { + // In this case, the return value of the pre-handler is made available on + // whatever the 'assign' option is in the route config. + return licenseInfo; + } else { + // In this case, the route handler is never called and the user gets this + // error message + throw Boom.forbidden(`You don't have the right license for MyPlugin!`); + } +} + +server.route({ + method: 'GET', + path: '/api/my-plugin/do-something', + config: { + pre: [{ method: licensePreRouting, assign: 'licenseInfo' }] + }, + handler: (req) => { + const licenseInfo = req.pre.licenseInfo; + return doSomethingInteresting(licenseInfo); + } +}) +``` + +In many cases, it may be simpler to duplicate the function call +to retrieve the data again in the main handler. In this other cases, you can +utilize a handler _factory_ rather than a raw handler as the argument to your +high-order handler. This way the high-order handler can pass arbitrary arguments +to the route handler. + +```ts +// New Platform high-order handler +const checkLicense = ( + handlerFactory: (licenseInfo: MyPluginLicenseInfo) => RequestHandler +): RequestHandler => { + return (context, req, res) => { + const licenseInfo = getMyPluginLicenseInfo(context.licensing.license); + + if (licenseInfo.hasAtLeast('gold')) { + const handler = handlerFactory(licenseInfo); + return handler(context, req, res); + } else { + return res.forbidden({ body: `You don't have the right license for MyPlugin!` }); + } + } +} + +router.get( + { path: '/api/my-plugin/do-something', validate: false }, + checkLicense(licenseInfo => async (context, req, res) => { + const results = doSomethingInteresting(licenseInfo); + return res.ok({ body: results }); + }), +) +``` + ## Chrome In the Legacy Platform, the `ui/chrome` import contained APIs for a very wide From 81f3fbbf2546f33c980477ad617f910f313bdf78 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Thu, 30 Jan 2020 15:06:52 -0600 Subject: [PATCH 06/62] [APM] Service map center button (#56434) Add center button for service map. The fullscreen button is still there until #56351 is merged. Add fit and padding to the layout animation. Make the node labels wider so they aren't cut off. --- .../components/app/ServiceMap/Controls.tsx | 28 +++++++++++++++++-- .../app/ServiceMap/cytoscapeOptions.ts | 18 +++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx index 5b7ddc2b450d60..07ea97f442b7fa 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Controls.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useState, useEffect } from 'react'; import { EuiButtonIcon, EuiPanel } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; +import React, { useContext, useEffect, useState } from 'react'; +import styled from 'styled-components'; import { CytoscapeContext } from './Cytoscape'; +import { animationOptions, nodeHeight } from './cytoscapeOptions'; import { FullscreenPanel } from './FullscreenPanel'; const ControlsContainer = styled('div')` @@ -58,6 +59,17 @@ export function Controls() { } }, [cy]); + function center() { + if (cy) { + const eles = cy.nodes(); + cy.animate({ + ...animationOptions, + center: { eles }, + fit: { eles, padding: nodeHeight } + }); + } + } + function zoomIn() { doZoom(cy, increment); } @@ -82,6 +94,9 @@ export function Controls() { const zoomOutLabel = i18n.translate('xpack.apm.serviceMap.zoomOut', { defaultMessage: 'Zoom out' }); + const centerLabel = i18n.translate('xpack.apm.serviceMap.center', { + defaultMessage: 'Center' + }); return ( @@ -103,6 +118,15 @@ export function Controls() { title={zoomOutLabel} /> + + ; + }; + + const ExtendedComponent = withActionOperations(ComponentToExtend); + const component = mount(); + component.find('button').simulate('click'); + + expect(actionApis.loadActionTypes).toHaveBeenCalledTimes(1); + expect(actionApis.loadActionTypes).toHaveBeenCalledWith({ http }); + }); +}); diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx new file mode 100644 index 00000000000000..45e6c6b10532ce --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_actions_api_operations.tsx @@ -0,0 +1,28 @@ +/* + * 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 { ActionType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { loadActionTypes } from '../../../lib/action_connector_api'; + +export interface ComponentOpts { + loadActionTypes: () => Promise; +} + +export type PropsWithOptionalApiHandlers = Omit & Partial; + +export function withActionOperations( + WrappedComponent: React.ComponentType +): React.FunctionComponent> { + return (props: PropsWithOptionalApiHandlers) => { + const { http } = useAppDependencies(); + return ( + loadActionTypes({ http })} /> + ); + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx new file mode 100644 index 00000000000000..30a065479ce33c --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx @@ -0,0 +1,269 @@ +/* + * 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 React from 'react'; +import { shallow, mount } from 'enzyme'; +import uuid from 'uuid'; +import { withBulkAlertOperations, ComponentOpts } from './with_bulk_alert_api_operations'; +import * as alertApi from '../../../lib/alert_api'; +import { useAppDependencies } from '../../../app_context'; +import { Alert } from '../../../../types'; + +jest.mock('../../../lib/alert_api'); + +jest.mock('../../../app_context', () => { + const http = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ + http, + legacy: { + capabilities: { + get: jest.fn(() => ({})), + }, + }, + })), + }; +}); + +describe('with_bulk_alert_api_operations', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('extends any component with AlertApi methods', () => { + const ComponentToExtend = (props: ComponentOpts) => { + expect(typeof props.muteAlerts).toEqual('function'); + expect(typeof props.unmuteAlerts).toEqual('function'); + expect(typeof props.enableAlerts).toEqual('function'); + expect(typeof props.disableAlerts).toEqual('function'); + expect(typeof props.deleteAlerts).toEqual('function'); + expect(typeof props.muteAlert).toEqual('function'); + expect(typeof props.unmuteAlert).toEqual('function'); + expect(typeof props.enableAlert).toEqual('function'); + expect(typeof props.disableAlert).toEqual('function'); + expect(typeof props.deleteAlert).toEqual('function'); + expect(typeof props.loadAlert).toEqual('function'); + expect(typeof props.loadAlertTypes).toEqual('function'); + return
; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + expect(shallow().type()).toEqual(ComponentToExtend); + }); + + // single alert + it('muteAlert calls the muteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.muteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('unmuteAlert calls the unmuteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ muteAll: true }); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('enableAlert calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert({ enabled: false }); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.enableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('disableAlert calls the disableAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.disableAlert).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + it('deleteAlert calls the deleteAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlert, alert }: ComponentOpts & { alert: Alert }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alert = mockAlert(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlert).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlert).toHaveBeenCalledWith({ id: alert.id, http }); + }); + + // bulk alerts + it('muteAlerts calls the muteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ muteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.muteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.muteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('unmuteAlerts calls the unmuteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ unmuteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert({ muteAll: true }), mockAlert({ muteAll: true })]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.unmuteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.unmuteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('enableAlerts calls the muteAlertss api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ enableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [ + mockAlert({ enabled: false }), + mockAlert({ enabled: true }), + mockAlert({ enabled: false }), + ]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.enableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.enableAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[2].id], http }); + }); + + it('disableAlerts calls the disableAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ disableAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.disableAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.disableAlerts).toHaveBeenCalledWith({ + ids: [alerts[0].id, alerts[1].id], + http, + }); + }); + + it('deleteAlerts calls the deleteAlerts api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ deleteAlerts, alerts }: ComponentOpts & { alerts: Alert[] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alerts = [mockAlert(), mockAlert()]; + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.deleteAlerts).toHaveBeenCalledTimes(1); + expect(alertApi.deleteAlerts).toHaveBeenCalledWith({ ids: [alerts[0].id, alerts[1].id], http }); + }); + + it('loadAlert calls the loadAlert api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ + loadAlert, + alertId, + }: ComponentOpts & { alertId: Alert['id'] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const alertId = uuid.v4(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.loadAlert).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlert).toHaveBeenCalledWith({ alertId, http }); + }); + + it('loadAlertTypes calls the loadAlertTypes api', () => { + const { http } = useAppDependencies(); + const ComponentToExtend = ({ loadAlertTypes }: ComponentOpts) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.loadAlertTypes).toHaveBeenCalledTimes(1); + expect(alertApi.loadAlertTypes).toHaveBeenCalledWith({ http }); + }); +}); + +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + ...overloads, + }; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx new file mode 100644 index 00000000000000..c61ba631ab8685 --- /dev/null +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -0,0 +1,103 @@ +/* + * 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 { Alert, AlertType } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { + deleteAlerts, + disableAlerts, + enableAlerts, + muteAlerts, + unmuteAlerts, + deleteAlert, + disableAlert, + enableAlert, + muteAlert, + unmuteAlert, + loadAlert, + loadAlertTypes, +} from '../../../lib/alert_api'; + +export interface ComponentOpts { + muteAlerts: (alerts: Alert[]) => Promise; + unmuteAlerts: (alerts: Alert[]) => Promise; + enableAlerts: (alerts: Alert[]) => Promise; + disableAlerts: (alerts: Alert[]) => Promise; + deleteAlerts: (alerts: Alert[]) => Promise; + muteAlert: (alert: Alert) => Promise; + unmuteAlert: (alert: Alert) => Promise; + enableAlert: (alert: Alert) => Promise; + disableAlert: (alert: Alert) => Promise; + deleteAlert: (alert: Alert) => Promise; + loadAlert: (id: Alert['id']) => Promise; + loadAlertTypes: () => Promise; +} + +export type PropsWithOptionalApiHandlers = Omit & Partial; + +export function withBulkAlertOperations( + WrappedComponent: React.ComponentType +): React.FunctionComponent> { + return (props: PropsWithOptionalApiHandlers) => { + const { http } = useAppDependencies(); + return ( + + muteAlerts({ http, ids: items.filter(item => !isAlertMuted(item)).map(item => item.id) }) + } + unmuteAlerts={async (items: Alert[]) => + unmuteAlerts({ http, ids: items.filter(isAlertMuted).map(item => item.id) }) + } + enableAlerts={async (items: Alert[]) => + enableAlerts({ http, ids: items.filter(isAlertDisabled).map(item => item.id) }) + } + disableAlerts={async (items: Alert[]) => + disableAlerts({ + http, + ids: items.filter(item => !isAlertDisabled(item)).map(item => item.id), + }) + } + deleteAlerts={async (items: Alert[]) => + deleteAlerts({ http, ids: items.map(item => item.id) }) + } + muteAlert={async (alert: Alert) => { + if (!isAlertMuted(alert)) { + return muteAlert({ http, id: alert.id }); + } + }} + unmuteAlert={async (alert: Alert) => { + if (isAlertMuted(alert)) { + return unmuteAlert({ http, id: alert.id }); + } + }} + enableAlert={async (alert: Alert) => { + if (isAlertDisabled(alert)) { + return enableAlert({ http, id: alert.id }); + } + }} + disableAlert={async (alert: Alert) => { + if (!isAlertDisabled(alert)) { + return disableAlert({ http, id: alert.id }); + } + }} + deleteAlert={async (alert: Alert) => deleteAlert({ http, id: alert.id })} + loadAlert={async (alertId: Alert['id']) => loadAlert({ http, alertId })} + loadAlertTypes={async () => loadAlertTypes({ http })} + /> + ); + }; +} + +function isAlertDisabled(alert: Alert) { + return alert.enabled === false; +} + +function isAlertMuted(alert: Alert) { + return alert.muteAll === true; +} diff --git a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts index ed63ade903104c..7fb7d0bf48e4d2 100644 --- a/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts +++ b/x-pack/legacy/plugins/triggers_actions_ui/np_ready/public/types.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { TypeRegistry } from './application/type_registry'; -import { SanitizedAlert as Alert } from '../../../alerting/common'; -export { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { SanitizedAlert as Alert, AlertAction } from '../../../alerting/common'; +import { ActionType } from '../../../../../plugins/actions/common'; + +export { Alert, AlertAction }; +export { ActionType }; export type ActionTypeIndex = Record; export type AlertTypeIndex = Record; @@ -47,11 +50,6 @@ export interface ValidationResult { errors: Record; } -export interface ActionType { - id: string; - name: string; -} - export interface ActionConnector { secrets: Record; id: string; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts new file mode 100644 index 00000000000000..e8ed54571c77cd --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -0,0 +1,146 @@ +/* + * 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 expect from '@kbn/expect'; +import uuid from 'uuid'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header', 'alertDetailsUI']); + const browser = getService('browser'); + const alerting = getService('alerting'); + + describe('Alert Details', function() { + const testRunUuid = uuid.v4(); + + before(async () => { + await pageObjects.common.navigateToApp('triggersActions'); + + const actions = await Promise.all([ + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${0}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + alerting.actions.createAction({ + name: `server-log-${testRunUuid}-${1}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }), + ]); + + const alert = await alerting.alerts.createAlwaysFiringWithActions( + `test-alert-${testRunUuid}`, + actions.map(action => ({ + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + })) + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + }); + + it('renders the alert details', async () => { + const headingText = await pageObjects.alertDetailsUI.getHeadingText(); + expect(headingText).to.be(`test-alert-${testRunUuid}`); + + const alertType = await pageObjects.alertDetailsUI.getAlertType(); + expect(alertType).to.be(`Always Firing`); + + const { actionType, actionCount } = await pageObjects.alertDetailsUI.getActionsLabels(); + expect(actionType).to.be(`Server log`); + expect(actionCount).to.be(`+1`); + }); + + it('should disable the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await enableSwitch.click(); + + const enabledSwitchAfterDisabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterDisabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + + it('shouldnt allow you to mute a disabled alert', async () => { + const disabledEnableSwitch = await testSubjects.find('enableSwitch'); + expect(await disabledEnableSwitch.getAttribute('aria-checked')).to.eql('false'); + + const muteSwitch = await testSubjects.find('muteSwitch'); + expect(await muteSwitch.getAttribute('aria-checked')).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterTryingToMute = await testSubjects.find('muteSwitch'); + const isDisabledMuteAfterDisabling = await muteSwitchAfterTryingToMute.getAttribute( + 'aria-checked' + ); + expect(isDisabledMuteAfterDisabling).to.eql('false'); + }); + + it('should reenable a disabled the alert', async () => { + const enableSwitch = await testSubjects.find('enableSwitch'); + + const isChecked = await enableSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await enableSwitch.click(); + + const enabledSwitchAfterReenabling = await testSubjects.find('enableSwitch'); + const isCheckedAfterDisabling = await enabledSwitchAfterReenabling.getAttribute( + 'aria-checked' + ); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should mute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('false'); + + await muteSwitch.click(); + + const muteSwitchAfterDisabling = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterDisabling.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('true'); + }); + + it('should unmute the alert', async () => { + const muteSwitch = await testSubjects.find('muteSwitch'); + + const isChecked = await muteSwitch.getAttribute('aria-checked'); + expect(isChecked).to.eql('true'); + + await muteSwitch.click(); + + const muteSwitchAfterUnmuting = await testSubjects.find('muteSwitch'); + const isCheckedAfterDisabling = await muteSwitchAfterUnmuting.getAttribute('aria-checked'); + expect(isCheckedAfterDisabling).to.eql('false'); + }); + }); +}; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts index 13f50a505b0b6f..307f39382a2363 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/home_page.ts @@ -12,6 +12,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const log = getService('log'); const browser = getService('browser'); + const alerting = getService('alerting'); describe('Home page', function() { before(async () => { @@ -55,6 +56,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify content await testSubjects.existOrFail('alertsList'); }); + + it('navigates to an alert details page', async () => { + const action = await alerting.actions.createAction({ + name: `server-log-${Date.now()}`, + actionTypeId: '.server-log', + config: {}, + secrets: {}, + }); + + const alert = await alerting.alerts.createAlwaysFiringWithAction( + `test-alert-${Date.now()}`, + { + id: action.id, + group: 'default', + params: { + message: 'from alert 1s', + level: 'warn', + }, + } + ); + + // refresh to see alert + await browser.refresh(); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + // Verify content + await testSubjects.existOrFail('alertsList'); + + // click on first alert + await pageObjects.triggersActionsUI.clickOnAlertInAlertsList(alert.name); + + // Verify url + expect(await browser.getCurrentUrl()).to.contain(`/alert/${alert.id}`); + + await alerting.alerts.deleteAlert(alert.id); + }); }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index c76f477c8cfbef..a771fbf85e0b6e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -12,5 +12,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./details')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts index df651c67c2c281..43162e92563703 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/index.ts @@ -12,13 +12,42 @@ export default function(kibana: any) { require: ['alerting'], name: 'alerts', init(server: any) { - const noopAlertType: AlertType = { - id: 'test.noop', - name: 'Test: Noop', - actionGroups: ['default'], - async executor() {}, - }; - server.plugins.alerting.setup.registerType(noopAlertType); + createNoopAlertType(server.plugins.alerting.setup); + createAlwaysFiringAlertType(server.plugins.alerting.setup); }, }); } + +function createNoopAlertType(setupContract: any) { + const noopAlertType: AlertType = { + id: 'test.noop', + name: 'Test: Noop', + actionGroups: ['default'], + async executor() {}, + }; + setupContract.registerType(noopAlertType); +} + +function createAlwaysFiringAlertType(setupContract: any) { + // Alert types + const alwaysFiringAlertType: any = { + id: 'test.always-firing', + name: 'Always Firing', + actionGroups: ['default', 'other'], + async executor(alertExecutorOptions: any) { + const { services, state } = alertExecutorOptions; + + services + .alertInstanceFactory('1') + .replaceState({ instanceStateValue: true }) + .scheduleActions('default', { + instanceContextValue: true, + }); + return { + globalStateValue: true, + groupInSeriesIndex: (state.groupInSeriesIndex || 0) + 1, + }; + }, + }; + setupContract.registerType(alwaysFiringAlertType); +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts new file mode 100644 index 00000000000000..6d2038a6ba04c3 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.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 { FtrProviderContext } from '../ftr_provider_context'; + +export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + async getHeadingText() { + return await testSubjects.getVisibleText('alertDetailsTitle'); + }, + async getAlertType() { + return await testSubjects.getVisibleText('alertTypeLabel'); + }, + async getActionsLabels() { + return { + actionType: await testSubjects.getVisibleText('actionTypeLabel'), + actionCount: await testSubjects.getVisibleText('actionCountLabel'), + }; + }, + }; +} diff --git a/x-pack/test/functional_with_es_ssl/page_objects/index.ts b/x-pack/test/functional_with_es_ssl/page_objects/index.ts index a068ba7dfe81d3..cfc44221a9c17f 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/index.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/index.ts @@ -6,8 +6,10 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; import { TriggersActionsPageProvider } from './triggers_actions_ui_page'; +import { AlertDetailsPageProvider } from './alert_details'; export const pageObjects = { ...xpackFunctionalPageObjects, triggersActionsUI: TriggersActionsPageProvider, + alertDetailsUI: AlertDetailsPageProvider, }; diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index a04ecc969a7e1f..ae66ac0ddddfb7 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -92,6 +92,10 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) }; }); }, + async clickOnAlertInAlertsList(name: string) { + await this.searchAlerts(name); + await find.clickDisplayedByCssSelector(`[data-test-subj="alertsList"] [title="${name}"]`); + }, async changeTabs(tab: 'alertsTab' | 'connectorsTab') { return await testSubjects.click(tab); }, diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts new file mode 100644 index 00000000000000..9454a32757068b --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/actions.ts @@ -0,0 +1,48 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Actions { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/actions' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAction(actionParams: { + name: string; + actionTypeId: string; + config: Record; + secrets: Record; + }) { + this.log.debug(`creating action ${actionParams.name}`); + + const { data: action, status: actionStatus, actionStatusText } = await this.axios.post( + `/api/action`, + actionParams + ); + if (actionStatus !== 200) { + throw new Error( + `Expected status code of 200, received ${actionStatus} ${actionStatusText}: ${util.inspect( + action + )}` + ); + } + + this.log.debug(`created action ${action.id}`); + return action; + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts new file mode 100644 index 00000000000000..1a31d4796d5bc4 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/alerts.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { ToolingLog } from '@kbn/dev-utils'; + +export class Alerts { + private log: ToolingLog; + private axios: AxiosInstance; + + constructor(url: string, log: ToolingLog) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/alerting/alerts' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async createAlwaysFiringWithActions( + name: string, + actions: Array<{ + id: string; + group: string; + params: Record; + }> + ) { + this.log.debug(`creating alert ${name}`); + + const { data: alert, status, statusText } = await this.axios.post(`/api/alert`, { + enabled: true, + name, + tags: ['foo'], + alertTypeId: 'test.always-firing', + consumer: 'bar', + schedule: { interval: '1m' }, + throttle: '1m', + actions, + params: {}, + }); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + + this.log.debug(`created alert ${alert.id}`); + + return alert; + } + + public async createAlwaysFiringWithAction( + name: string, + action: { + id: string; + group: string; + params: Record; + } + ) { + return this.createAlwaysFiringWithActions(name, [action]); + } + + public async deleteAlert(id: string) { + this.log.debug(`deleting alert ${id}`); + + const { data: alert, status, statusText } = await this.axios.delete(`/api/alert/${id}`); + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(alert)}` + ); + } + this.log.debug(`deleted alert ${alert.id}`); + } +} diff --git a/x-pack/test/functional_with_es_ssl/services/alerting/index.ts b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts new file mode 100644 index 00000000000000..e0aa827316c01a --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/services/alerting/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { format as formatUrl } from 'url'; + +import { Alerts } from './alerts'; +import { Actions } from './actions'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function AlertsServiceProvider({ getService }: FtrProviderContext) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new (class AlertingService { + actions = new Actions(url, log); + alerts = new Alerts(url, log); + })(); +} diff --git a/x-pack/test/functional_with_es_ssl/services/index.ts b/x-pack/test/functional_with_es_ssl/services/index.ts index 6e96921c25a316..f04c2c980055d3 100644 --- a/x-pack/test/functional_with_es_ssl/services/index.ts +++ b/x-pack/test/functional_with_es_ssl/services/index.ts @@ -5,7 +5,9 @@ */ import { services as xpackFunctionalServices } from '../../functional/services'; +import { AlertsServiceProvider } from './alerting'; export const services = { ...xpackFunctionalServices, + alerting: AlertsServiceProvider, }; From 38dc1cbd3cb74baeb498978688c92683d42b974e Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 3 Feb 2020 12:03:50 +0100 Subject: [PATCH 34/62] Migrate es-archiver to typescript (#56008) * migrate lib/archives and lib/docs * migrate lib/indices * migrate end of /lib * migrate /actions * migrate es_archiver * migrate cli * migrate tests * use proper log stub * add Record typing Co-authored-by: Elastic Machine --- .../lib/config/read_config_file.ts | 2 +- src/es_archiver/actions/{edit.js => edit.ts} | 13 +- ..._kibana_index.js => empty_kibana_index.ts} | 18 +- .../actions/{index.js => index.ts} | 0 src/es_archiver/actions/{load.js => load.ts} | 29 +++- .../{rebuild_all.js => rebuild_all.ts} | 21 ++- src/es_archiver/actions/{save.js => save.ts} | 24 ++- .../actions/{unload.js => unload.ts} | 22 ++- src/es_archiver/{cli.js => cli.ts} | 22 +-- src/es_archiver/es_archiver.d.ts | 39 ----- .../{es_archiver.js => es_archiver.ts} | 33 ++-- src/es_archiver/index.js | 20 --- src/es_archiver/{index.d.ts => index.ts} | 0 .../lib/__tests__/{stats.js => stats.ts} | 18 +- .../__tests__/{format.js => format.ts} | 16 +- .../archives/__tests__/{parse.js => parse.ts} | 20 +-- .../archives/{constants.js => constants.ts} | 0 .../archives/{filenames.js => filenames.ts} | 6 +- .../lib/archives/{format.js => format.ts} | 4 +- .../lib/archives/{index.js => index.ts} | 2 - .../lib/archives/{parse.js => parse.ts} | 4 +- .../lib/{directory.js => directory.ts} | 5 +- ...ream.js => generate_doc_records_stream.ts} | 2 +- ..._stream.js => index_doc_records_stream.ts} | 4 +- .../lib/docs/__tests__/{stubs.js => stubs.ts} | 29 +++- ...ream.js => generate_doc_records_stream.ts} | 19 ++- .../lib/docs/{index.js => index.ts} | 0 ..._stream.js => index_doc_records_stream.ts} | 9 +- src/es_archiver/lib/{index.js => index.ts} | 2 +- ...index_stream.js => create_index_stream.ts} | 51 +++--- ...index_stream.js => delete_index_stream.ts} | 29 ++-- ...am.js => generate_index_records_stream.ts} | 14 +- .../lib/indices/__tests__/stubs.js | 128 --------------- .../lib/indices/__tests__/stubs.ts | 154 ++++++++++++++++++ ...index_stream.js => create_index_stream.ts} | 35 +++- .../{delete_index.js => delete_index.ts} | 34 ++-- ...index_stream.js => delete_index_stream.ts} | 10 +- ...am.js => generate_index_records_stream.ts} | 8 +- .../lib/indices/{index.js => index.ts} | 0 .../{kibana_index.js => kibana_index.ts} | 96 +++++++---- ...rds_stream.js => filter_records_stream.ts} | 2 +- ...rds_stream.js => filter_records_stream.ts} | 4 +- .../lib/records/{index.js => index.ts} | 0 src/es_archiver/lib/stats.ts | 2 + src/legacy/utils/index.d.ts | 13 ++ src/legacy/utils/streams/index.d.ts | 6 +- 46 files changed, 576 insertions(+), 393 deletions(-) rename src/es_archiver/actions/{edit.js => edit.ts} (91%) rename src/es_archiver/actions/{empty_kibana_index.js => empty_kibana_index.ts} (73%) rename src/es_archiver/actions/{index.js => index.ts} (100%) rename src/es_archiver/actions/{load.js => load.ts} (84%) rename src/es_archiver/actions/{rebuild_all.js => rebuild_all.ts} (84%) rename src/es_archiver/actions/{save.js => save.ts} (83%) rename src/es_archiver/actions/{unload.js => unload.ts} (79%) rename src/es_archiver/{cli.js => cli.ts} (90%) delete mode 100644 src/es_archiver/es_archiver.d.ts rename src/es_archiver/{es_archiver.js => es_archiver.ts} (83%) delete mode 100644 src/es_archiver/index.js rename src/es_archiver/{index.d.ts => index.ts} (100%) rename src/es_archiver/lib/__tests__/{stats.js => stats.ts} (95%) rename src/es_archiver/lib/archives/__tests__/{format.js => format.ts} (89%) rename src/es_archiver/lib/archives/__tests__/{parse.js => parse.ts} (93%) rename src/es_archiver/lib/archives/{constants.js => constants.ts} (100%) rename src/es_archiver/lib/archives/{filenames.js => filenames.ts} (91%) rename src/es_archiver/lib/archives/{format.js => format.ts} (93%) rename src/es_archiver/lib/archives/{index.js => index.ts} (99%) rename src/es_archiver/lib/archives/{parse.js => parse.ts} (91%) rename src/es_archiver/lib/{directory.js => directory.ts} (88%) rename src/es_archiver/lib/docs/__tests__/{generate_doc_records_stream.js => generate_doc_records_stream.ts} (98%) rename src/es_archiver/lib/docs/__tests__/{index_doc_records_stream.js => index_doc_records_stream.ts} (98%) rename src/es_archiver/lib/docs/__tests__/{stubs.js => stubs.ts} (74%) rename src/es_archiver/lib/docs/{generate_doc_records_stream.js => generate_doc_records_stream.ts} (80%) rename src/es_archiver/lib/docs/{index.js => index.ts} (100%) rename src/es_archiver/lib/docs/{index_doc_records_stream.js => index_doc_records_stream.ts} (86%) rename src/es_archiver/lib/{index.js => index.ts} (96%) rename src/es_archiver/lib/indices/__tests__/{create_index_stream.js => create_index_stream.ts} (76%) rename src/es_archiver/lib/indices/__tests__/{delete_index_stream.js => delete_index_stream.ts} (66%) rename src/es_archiver/lib/indices/__tests__/{generate_index_records_stream.js => generate_index_records_stream.ts} (89%) delete mode 100644 src/es_archiver/lib/indices/__tests__/stubs.js create mode 100644 src/es_archiver/lib/indices/__tests__/stubs.ts rename src/es_archiver/lib/indices/{create_index_stream.js => create_index_stream.ts} (81%) rename src/es_archiver/lib/indices/{delete_index.js => delete_index.ts} (76%) rename src/es_archiver/lib/indices/{delete_index_stream.js => delete_index_stream.ts} (86%) rename src/es_archiver/lib/indices/{generate_index_records_stream.js => generate_index_records_stream.ts} (89%) rename src/es_archiver/lib/indices/{index.js => index.ts} (100%) rename src/es_archiver/lib/indices/{kibana_index.js => kibana_index.ts} (70%) rename src/es_archiver/lib/records/__tests__/{filter_records_stream.js => filter_records_stream.ts} (97%) rename src/es_archiver/lib/records/{filter_records_stream.js => filter_records_stream.ts} (91%) rename src/es_archiver/lib/records/{index.js => index.ts} (100%) diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 72926cae7dbc4b..96fd525efa3ec8 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -56,7 +56,7 @@ async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrid return transformDeprecations(settingsWithDefaults, logDeprecation); } -export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any) { +export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any = {}) { return new Config({ settings: await getSettingsFromFile(log, path, settingOverrides), primary: true, diff --git a/src/es_archiver/actions/edit.js b/src/es_archiver/actions/edit.ts similarity index 91% rename from src/es_archiver/actions/edit.js rename to src/es_archiver/actions/edit.ts index 5e3a3490133c71..de63081a1ea1b5 100644 --- a/src/es_archiver/actions/edit.js +++ b/src/es_archiver/actions/edit.ts @@ -22,12 +22,23 @@ import Fs from 'fs'; import { createGunzip, createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { promisify } from 'util'; import globby from 'globby'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; const unlinkAsync = promisify(Fs.unlink); -export async function editAction({ prefix, dataDir, log, handler }) { +export async function editAction({ + prefix, + dataDir, + log, + handler, +}: { + prefix: string; + dataDir: string; + log: ToolingLog; + handler: () => Promise; +}) { const archives = ( await globby('**/*.gz', { cwd: prefix ? resolve(dataDir, prefix) : dataDir, diff --git a/src/es_archiver/actions/empty_kibana_index.js b/src/es_archiver/actions/empty_kibana_index.ts similarity index 73% rename from src/es_archiver/actions/empty_kibana_index.js rename to src/es_archiver/actions/empty_kibana_index.ts index 386863ec18a43d..5f96fbc5f996ca 100644 --- a/src/es_archiver/actions/empty_kibana_index.js +++ b/src/es_archiver/actions/empty_kibana_index.ts @@ -16,13 +16,25 @@ * specific language governing permissions and limitations * under the License. */ + +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; + import { migrateKibanaIndex, deleteKibanaIndices, createStats } from '../lib'; -export async function emptyKibanaIndexAction({ client, log, kbnClient }) { +export async function emptyKibanaIndexAction({ + client, + log, + kbnClient, +}: { + client: Client; + log: ToolingLog; + kbnClient: KbnClient; +}) { const stats = createStats('emptyKibanaIndex', log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); - await deleteKibanaIndices({ client, stats }); - await migrateKibanaIndex({ client, log, stats, kibanaPluginIds }); + await deleteKibanaIndices({ client, stats, log }); + await migrateKibanaIndex({ client, log, kibanaPluginIds }); return stats; } diff --git a/src/es_archiver/actions/index.js b/src/es_archiver/actions/index.ts similarity index 100% rename from src/es_archiver/actions/index.js rename to src/es_archiver/actions/index.ts diff --git a/src/es_archiver/actions/load.js b/src/es_archiver/actions/load.ts similarity index 84% rename from src/es_archiver/actions/load.js rename to src/es_archiver/actions/load.ts index ea02ce9dd3ad33..404fd0daea91d4 100644 --- a/src/es_archiver/actions/load.js +++ b/src/es_archiver/actions/load.ts @@ -19,6 +19,9 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable } from 'stream'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; import { createPromiseFromStreams, concatStreamProviders } from '../../legacy/utils'; @@ -38,12 +41,26 @@ import { // pipe a series of streams into each other so that data and errors // flow from the first stream to the last. Errors from the last stream // are not listened for -const pipeline = (...streams) => +const pipeline = (...streams: Readable[]) => streams.reduce((source, dest) => - source.once('error', error => dest.emit('error', error)).pipe(dest) + source.once('error', error => dest.emit('error', error)).pipe(dest as any) ); -export async function loadAction({ name, skipExisting, client, dataDir, log, kbnClient }) { +export async function loadAction({ + name, + skipExisting, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + skipExisting: boolean; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const files = prioritizeMappings(await readDirectory(inputDir)); @@ -64,12 +81,12 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn { objectMode: true } ); - const progress = new Progress('load progress'); + const progress = new Progress(); progress.activate(log); await createPromiseFromStreams([ recordStream, - createCreateIndexStream({ client, stats, skipExisting, log, kibanaPluginIds }), + createCreateIndexStream({ client, stats, skipExisting, log }), createIndexDocRecordsStream(client, stats, progress), ]); @@ -77,7 +94,7 @@ export async function loadAction({ name, skipExisting, client, dataDir, log, kbn const result = stats.toJSON(); for (const [index, { docs }] of Object.entries(result)) { - if (!docs && docs.indexed > 0) { + if (docs && docs.indexed > 0) { log.info('[%s] Indexed %d docs into %j', name, docs.indexed, index); } } diff --git a/src/es_archiver/actions/rebuild_all.js b/src/es_archiver/actions/rebuild_all.ts similarity index 84% rename from src/es_archiver/actions/rebuild_all.js rename to src/es_archiver/actions/rebuild_all.ts index 9379a29c38130c..1467a1d0430b76 100644 --- a/src/es_archiver/actions/rebuild_all.js +++ b/src/es_archiver/actions/rebuild_all.ts @@ -18,13 +18,12 @@ */ import { resolve, dirname, relative } from 'path'; - import { stat, rename, createReadStream, createWriteStream } from 'fs'; - +import { Readable, Writable } from 'stream'; import { fromNode } from 'bluebird'; +import { ToolingLog } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { prioritizeMappings, readDirectory, @@ -33,12 +32,20 @@ import { createFormatArchiveStreams, } from '../lib'; -async function isDirectory(path) { +async function isDirectory(path: string): Promise { const stats = await fromNode(cb => stat(path, cb)); return stats.isDirectory(); } -export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { +export async function rebuildAllAction({ + dataDir, + log, + rootDir = dataDir, +}: { + dataDir: string; + log: ToolingLog; + rootDir?: string; +}) { const childNames = prioritizeMappings(await readDirectory(dataDir)); for (const childName of childNames) { const childPath = resolve(dataDir, childName); @@ -58,11 +65,11 @@ export async function rebuildAllAction({ dataDir, log, rootDir = dataDir }) { const tempFile = childPath + (gzip ? '.rebuilding.gz' : '.rebuilding'); await createPromiseFromStreams([ - createReadStream(childPath), + createReadStream(childPath) as Readable, ...createParseArchiveStreams({ gzip }), ...createFormatArchiveStreams({ gzip }), createWriteStream(tempFile), - ]); + ] as [Readable, ...Writable[]]); await fromNode(cb => rename(tempFile, childPath, cb)); log.info(`${archiveName} Rebuilt ${childName}`); diff --git a/src/es_archiver/actions/save.js b/src/es_archiver/actions/save.ts similarity index 83% rename from src/es_archiver/actions/save.js rename to src/es_archiver/actions/save.ts index 2c264ed2ee3a9e..7a3a9dd97c0ab1 100644 --- a/src/es_archiver/actions/save.js +++ b/src/es_archiver/actions/save.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createWriteStream, mkdirSync } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; import { createListStream, createPromiseFromStreams } from '../../legacy/utils'; - import { createStats, createGenerateIndexRecordsStream, @@ -30,7 +32,21 @@ import { Progress, } from '../lib'; -export async function saveAction({ name, indices, client, dataDir, log, raw }) { +export async function saveAction({ + name, + indices, + client, + dataDir, + log, + raw, +}: { + name: string; + indices: string | string[]; + client: Client; + dataDir: string; + log: ToolingLog; + raw: boolean; +}) { const outputDir = resolve(dataDir, name); const stats = createStats(name, log); @@ -48,7 +64,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateIndexRecordsStream(client, stats), ...createFormatArchiveStreams(), createWriteStream(resolve(outputDir, 'mappings.json')), - ]), + ] as [Readable, ...Writable[]]), // export all documents from matching indexes into data.json.gz createPromiseFromStreams([ @@ -56,7 +72,7 @@ export async function saveAction({ name, indices, client, dataDir, log, raw }) { createGenerateDocRecordsStream(client, stats, progress), ...createFormatArchiveStreams({ gzip: !raw }), createWriteStream(resolve(outputDir, `data.json${raw ? '' : '.gz'}`)), - ]), + ] as [Readable, ...Writable[]]), ]); progress.deactivate(); diff --git a/src/es_archiver/actions/unload.js b/src/es_archiver/actions/unload.ts similarity index 79% rename from src/es_archiver/actions/unload.js rename to src/es_archiver/actions/unload.ts index 2acf8d2d719865..130a6b542b218a 100644 --- a/src/es_archiver/actions/unload.js +++ b/src/es_archiver/actions/unload.ts @@ -19,9 +19,11 @@ import { resolve } from 'path'; import { createReadStream } from 'fs'; +import { Readable, Writable } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { createPromiseFromStreams } from '../../legacy/utils'; - import { isGzip, createStats, @@ -32,7 +34,19 @@ import { createDeleteIndexStream, } from '../lib'; -export async function unloadAction({ name, client, dataDir, log, kbnClient }) { +export async function unloadAction({ + name, + client, + dataDir, + log, + kbnClient, +}: { + name: string; + client: Client; + dataDir: string; + log: ToolingLog; + kbnClient: KbnClient; +}) { const inputDir = resolve(dataDir, name); const stats = createStats(name, log); const kibanaPluginIds = await kbnClient.plugins.getEnabledIds(); @@ -42,11 +56,11 @@ export async function unloadAction({ name, client, dataDir, log, kbnClient }) { log.info('[%s] Unloading indices from %j', name, filename); await createPromiseFromStreams([ - createReadStream(resolve(inputDir, filename)), + createReadStream(resolve(inputDir, filename)) as Readable, ...createParseArchiveStreams({ gzip: isGzip(filename) }), createFilterRecordsStream('index'), createDeleteIndexStream(client, stats, log, kibanaPluginIds), - ]); + ] as [Readable, ...Writable[]]); } return stats.toJSON(); diff --git a/src/es_archiver/cli.js b/src/es_archiver/cli.ts similarity index 90% rename from src/es_archiver/cli.js rename to src/es_archiver/cli.ts index 56d1fdca89780c..252f99f8f47afb 100644 --- a/src/es_archiver/cli.js +++ b/src/es_archiver/cli.ts @@ -17,7 +17,7 @@ * under the License. */ -/************************************************************* +/** *********************************************************** * * Run `node scripts/es_archiver --help` for usage information * @@ -27,17 +27,17 @@ import { resolve } from 'path'; import { readFileSync } from 'fs'; import { format as formatUrl } from 'url'; import readline from 'readline'; - import { Command } from 'commander'; import * as legacyElasticsearch from 'elasticsearch'; -import { EsArchiver } from './es_archiver'; import { ToolingLog } from '@kbn/dev-utils'; import { readConfigFile } from '@kbn/test'; +import { EsArchiver } from './es_archiver'; + const cmd = new Command('node scripts/es_archiver'); -const resolveConfigPath = v => resolve(process.cwd(), v); +const resolveConfigPath = (v: string) => resolve(process.cwd(), v); const defaultConfigPath = resolveConfigPath('test/functional/config.js'); cmd @@ -56,6 +56,7 @@ cmd defaultConfigPath ) .on('--help', () => { + // eslint-disable-next-line no-console console.log(readFileSync(resolve(__dirname, './cli_help.txt'), 'utf8')); }); @@ -95,10 +96,10 @@ cmd output: process.stdout, }); - await new Promise(resolve => { + await new Promise(resolveInput => { rl.question(`Press enter when you're done`, () => { rl.close(); - resolve(); + resolveInput(); }); }); }) @@ -112,12 +113,12 @@ cmd cmd.parse(process.argv); -const missingCommand = cmd.args.every(a => !(a instanceof Command)); +const missingCommand = cmd.args.every(a => !((a as any) instanceof Command)); if (missingCommand) { execute(); } -async function execute(fn) { +async function execute(fn?: (esArchiver: EsArchiver, command: Command) => void): Promise { try { const log = new ToolingLog({ level: cmd.verbose ? 'debug' : 'info', @@ -134,7 +135,7 @@ async function execute(fn) { // log and count all validation errors let errorCount = 0; - const error = msg => { + const error = (msg: string) => { errorCount++; log.error(msg); }; @@ -170,11 +171,12 @@ async function execute(fn) { dataDir: resolve(cmd.dir), kibanaUrl: cmd.kibanaUrl, }); - await fn(esArchiver, cmd); + await fn!(esArchiver, cmd); } finally { await client.close(); } } catch (err) { + // eslint-disable-next-line no-console console.log('FATAL ERROR', err.stack); } } diff --git a/src/es_archiver/es_archiver.d.ts b/src/es_archiver/es_archiver.d.ts deleted file mode 100644 index c50ae19d99cbf3..00000000000000 --- a/src/es_archiver/es_archiver.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { ToolingLog } from '@kbn/dev-utils'; -import { Client } from 'elasticsearch'; -import { createStats } from './lib/stats'; - -export type JsonStats = ReturnType['toJSON']>; - -export class EsArchiver { - constructor(options: { client: Client; dataDir: string; log: ToolingLog; kibanaUrl: string }); - public save( - name: string, - indices: string | string[], - options?: { raw?: boolean } - ): Promise; - public load(name: string, options?: { skipExisting?: boolean }): Promise; - public unload(name: string): Promise; - public rebuildAll(): Promise; - public edit(prefix: string, handler: () => Promise): Promise; - public loadIfNeeded(name: string): Promise; - public emptyKibanaIndex(): Promise; -} diff --git a/src/es_archiver/es_archiver.js b/src/es_archiver/es_archiver.ts similarity index 83% rename from src/es_archiver/es_archiver.js rename to src/es_archiver/es_archiver.ts index 705706d0e58778..5614dfd842087b 100644 --- a/src/es_archiver/es_archiver.js +++ b/src/es_archiver/es_archiver.ts @@ -17,7 +17,8 @@ * under the License. */ -import { KbnClient } from '@kbn/dev-utils'; +import { Client } from 'elasticsearch'; +import { ToolingLog, KbnClient } from '@kbn/dev-utils'; import { saveAction, @@ -29,7 +30,22 @@ import { } from './actions'; export class EsArchiver { - constructor({ client, dataDir, log, kibanaUrl }) { + private readonly client: Client; + private readonly dataDir: string; + private readonly log: ToolingLog; + private readonly kbnClient: KbnClient; + + constructor({ + client, + dataDir, + log, + kibanaUrl, + }: { + client: Client; + dataDir: string; + log: ToolingLog; + kibanaUrl: string; + }) { this.client = client; this.dataDir = dataDir; this.log = log; @@ -46,7 +62,7 @@ export class EsArchiver { * @property {Boolean} options.raw - should the archive be raw (unzipped) or not * @return Promise */ - async save(name, indices, { raw = false } = {}) { + async save(name: string, indices: string | string[], { raw = false }: { raw?: boolean } = {}) { return await saveAction({ name, indices, @@ -66,9 +82,7 @@ export class EsArchiver { * be ignored or overwritten * @return Promise */ - async load(name, options = {}) { - const { skipExisting } = options; - + async load(name: string, { skipExisting = false }: { skipExisting?: boolean } = {}) { return await loadAction({ name, skipExisting: !!skipExisting, @@ -85,7 +99,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async unload(name) { + async unload(name: string) { return await unloadAction({ name, client: this.client, @@ -103,7 +117,6 @@ export class EsArchiver { */ async rebuildAll() { return await rebuildAllAction({ - client: this.client, dataDir: this.dataDir, log: this.log, }); @@ -117,7 +130,7 @@ export class EsArchiver { * @param {() => Promise} handler * @return Promise */ - async edit(prefix, handler) { + async edit(prefix: string, handler: () => Promise) { return await editAction({ prefix, log: this.log, @@ -132,7 +145,7 @@ export class EsArchiver { * @param {String} name * @return Promise */ - async loadIfNeeded(name) { + async loadIfNeeded(name: string) { return await this.load(name, { skipExisting: true }); } diff --git a/src/es_archiver/index.js b/src/es_archiver/index.js deleted file mode 100644 index f7a579a98a42d9..00000000000000 --- a/src/es_archiver/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { EsArchiver } from './es_archiver'; diff --git a/src/es_archiver/index.d.ts b/src/es_archiver/index.ts similarity index 100% rename from src/es_archiver/index.d.ts rename to src/es_archiver/index.ts diff --git a/src/es_archiver/lib/__tests__/stats.js b/src/es_archiver/lib/__tests__/stats.ts similarity index 95% rename from src/es_archiver/lib/__tests__/stats.js rename to src/es_archiver/lib/__tests__/stats.ts index ccc24c25fb8601..28e337b3da5298 100644 --- a/src/es_archiver/lib/__tests__/stats.js +++ b/src/es_archiver/lib/__tests__/stats.ts @@ -17,26 +17,26 @@ * under the License. */ -import expect from '@kbn/expect'; import { uniq } from 'lodash'; import sinon from 'sinon'; +import expect from '@kbn/expect'; +import { ToolingLog } from '@kbn/dev-utils'; import { createStats } from '../'; -import { ToolingLog } from '@kbn/dev-utils'; -function createBufferedLog() { - const log = new ToolingLog({ +function createBufferedLog(): ToolingLog & { buffer: string } { + const log: ToolingLog = new ToolingLog({ level: 'debug', writeTo: { - write: chunk => (log.buffer += chunk), + write: chunk => ((log as any).buffer += chunk), }, }); - log.buffer = ''; - return log; + (log as any).buffer = ''; + return log as ToolingLog & { buffer: string }; } -function assertDeepClones(a, b) { - const path = []; +function assertDeepClones(a: any, b: any) { + const path: string[] = []; try { (function recurse(one, two) { if (typeof one !== 'object' || typeof two !== 'object') { diff --git a/src/es_archiver/lib/archives/__tests__/format.js b/src/es_archiver/lib/archives/__tests__/format.ts similarity index 89% rename from src/es_archiver/lib/archives/__tests__/format.js rename to src/es_archiver/lib/archives/__tests__/format.ts index 20ead30824d064..f472f094134d7b 100644 --- a/src/es_archiver/lib/archives/__tests__/format.js +++ b/src/es_archiver/lib/archives/__tests__/format.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream from 'stream'; +import Stream, { Readable, Writable } from 'stream'; import { createGunzip } from 'zlib'; import expect from '@kbn/expect'; @@ -43,11 +43,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -58,7 +58,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams({ gzip: false }), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); @@ -73,11 +73,11 @@ describe('esArchiver createFormatArchiveStreams', () => { }); it('streams consume js values and produces buffers', async () => { - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([1, 2, { foo: 'bar' }, [1, 2]]), ...createFormatArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output.length).to.be.greaterThan(0); output.forEach(b => expect(b).to.be.a(Buffer)); @@ -89,7 +89,7 @@ describe('esArchiver createFormatArchiveStreams', () => { ...createFormatArchiveStreams({ gzip: true }), createGunzip(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.be(INPUT_JSON); }); }); @@ -100,7 +100,7 @@ describe('esArchiver createFormatArchiveStreams', () => { createListStream(INPUTS), ...createFormatArchiveStreams(), createConcatStream(''), - ]); + ] as [Readable, ...Writable[]]); expect(json).to.be(INPUT_JSON); }); diff --git a/src/es_archiver/lib/archives/__tests__/parse.js b/src/es_archiver/lib/archives/__tests__/parse.ts similarity index 93% rename from src/es_archiver/lib/archives/__tests__/parse.js rename to src/es_archiver/lib/archives/__tests__/parse.ts index 2e1506e543a358..ba30156b5af393 100644 --- a/src/es_archiver/lib/archives/__tests__/parse.js +++ b/src/es_archiver/lib/archives/__tests__/parse.ts @@ -17,7 +17,7 @@ * under the License. */ -import Stream, { PassThrough, Transform } from 'stream'; +import Stream, { PassThrough, Readable, Writable, Transform } from 'stream'; import { createGzip } from 'zlib'; import expect from '@kbn/expect'; @@ -66,13 +66,13 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, 1]); }); it('provides each JSON object as soon as it is parsed', async () => { - let onReceived; + let onReceived: (resolved: any) => void; const receivedPromise = new Promise(resolve => (onReceived = resolve)); const input = new PassThrough(); const check = new Transform({ @@ -80,16 +80,16 @@ describe('esArchiver createParseArchiveStreams', () => { readableObjectMode: true, transform(chunk, env, callback) { onReceived(chunk); - callback(null, chunk); + callback(undefined, chunk); }, }); const finalPromise = createPromiseFromStreams([ - input, + input as Readable, ...createParseArchiveStreams(), check, createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); input.write(Buffer.from('{"a": 1}\n\n{"a":')); expect(await receivedPromise).to.eql({ a: 1 }); @@ -110,7 +110,7 @@ describe('esArchiver createParseArchiveStreams', () => { ]), ...createParseArchiveStreams({ gzip: false }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('Unexpected number'); @@ -149,7 +149,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([{ a: 1 }, { a: 2 }]); }); @@ -161,7 +161,7 @@ describe('esArchiver createParseArchiveStreams', () => { createGzip(), ...createParseArchiveStreams({ gzip: true }), createConcatStream([]), - ]); + ] as [Readable, ...Writable[]]); expect(output).to.eql([]); }); @@ -173,7 +173,7 @@ describe('esArchiver createParseArchiveStreams', () => { createListStream([Buffer.from('{"a": 1}')]), ...createParseArchiveStreams({ gzip: true }), createConcatStream(), - ]); + ] as [Readable, ...Writable[]]); throw new Error('should have failed'); } catch (err) { expect(err.message).to.contain('incorrect header check'); diff --git a/src/es_archiver/lib/archives/constants.js b/src/es_archiver/lib/archives/constants.ts similarity index 100% rename from src/es_archiver/lib/archives/constants.js rename to src/es_archiver/lib/archives/constants.ts diff --git a/src/es_archiver/lib/archives/filenames.js b/src/es_archiver/lib/archives/filenames.ts similarity index 91% rename from src/es_archiver/lib/archives/filenames.js rename to src/es_archiver/lib/archives/filenames.ts index 4ced04401d28d5..24c355edda278a 100644 --- a/src/es_archiver/lib/archives/filenames.js +++ b/src/es_archiver/lib/archives/filenames.ts @@ -19,7 +19,7 @@ import { basename, extname } from 'path'; -export function isGzip(path) { +export function isGzip(path: string) { return extname(path) === '.gz'; } @@ -28,7 +28,7 @@ export function isGzip(path) { * @param {String} path * @return {Boolean} */ -export function isMappingFile(path) { +export function isMappingFile(path: string) { return basename(path, '.gz') === 'mappings.json'; } @@ -41,7 +41,7 @@ export function isMappingFile(path) { * @param {Array} filenames * @return {Array} */ -export function prioritizeMappings(filenames) { +export function prioritizeMappings(filenames: string[]) { return filenames.slice().sort((fa, fb) => { if (isMappingFile(fa) === isMappingFile(fb)) return 0; return isMappingFile(fb) ? 1 : -1; diff --git a/src/es_archiver/lib/archives/format.js b/src/es_archiver/lib/archives/format.ts similarity index 93% rename from src/es_archiver/lib/archives/format.js rename to src/es_archiver/lib/archives/format.ts index 01fca87e7ba988..9bef4c9adbf059 100644 --- a/src/es_archiver/lib/archives/format.js +++ b/src/es_archiver/lib/archives/format.ts @@ -19,14 +19,12 @@ import { createGzip, Z_BEST_COMPRESSION } from 'zlib'; import { PassThrough } from 'stream'; - import stringify from 'json-stable-stringify'; import { createMapStream, createIntersperseStream } from '../../../legacy/utils'; - import { RECORD_SEPARATOR } from './constants'; -export function createFormatArchiveStreams({ gzip = false } = {}) { +export function createFormatArchiveStreams({ gzip = false }: { gzip?: boolean } = {}) { return [ createMapStream(record => stringify(record, { space: ' ' })), createIntersperseStream(RECORD_SEPARATOR), diff --git a/src/es_archiver/lib/archives/index.js b/src/es_archiver/lib/archives/index.ts similarity index 99% rename from src/es_archiver/lib/archives/index.js rename to src/es_archiver/lib/archives/index.ts index 4020f52e45a35b..6aa489ea5a46de 100644 --- a/src/es_archiver/lib/archives/index.js +++ b/src/es_archiver/lib/archives/index.ts @@ -18,7 +18,5 @@ */ export { isGzip, prioritizeMappings } from './filenames'; - export { createParseArchiveStreams } from './parse'; - export { createFormatArchiveStreams } from './format'; diff --git a/src/es_archiver/lib/archives/parse.js b/src/es_archiver/lib/archives/parse.ts similarity index 91% rename from src/es_archiver/lib/archives/parse.js rename to src/es_archiver/lib/archives/parse.ts index 4fe1df72592292..0f4460c925019d 100644 --- a/src/es_archiver/lib/archives/parse.js +++ b/src/es_archiver/lib/archives/parse.ts @@ -29,7 +29,7 @@ export function createParseArchiveStreams({ gzip = false } = {}) { gzip ? createGunzip() : new PassThrough(), createReplaceStream('\r\n', '\n'), createSplitStream(RECORD_SEPARATOR), - createFilterStream(l => l.match(/[^\s]/)), - createMapStream(json => JSON.parse(json.trim())), + createFilterStream(l => !!l.match(/[^\s]/)), + createMapStream(json => JSON.parse(json.trim())), ]; } diff --git a/src/es_archiver/lib/directory.js b/src/es_archiver/lib/directory.ts similarity index 88% rename from src/es_archiver/lib/directory.js rename to src/es_archiver/lib/directory.ts index 5aee10cfea65d9..8581207fa795d1 100644 --- a/src/es_archiver/lib/directory.js +++ b/src/es_archiver/lib/directory.ts @@ -18,10 +18,9 @@ */ import { readdir } from 'fs'; - import { fromNode } from 'bluebird'; -export async function readDirectory(path) { - const allNames = await fromNode(cb => readdir(path, cb)); +export async function readDirectory(path: string) { + const allNames = await fromNode(cb => readdir(path, cb)); return allNames.filter(name => !name.startsWith('.')); } diff --git a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts index bf4aab208127f4..03599cdc9fbcfe 100644 --- a/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/generate_doc_records_stream.ts @@ -143,7 +143,7 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { }, }, ]); - sinon.assert.calledTwice(stats.archivedDoc); + sinon.assert.calledTwice(stats.archivedDoc as any); expect(progress.getTotal()).to.be(2); expect(progress.getComplete()).to.be(2); }); diff --git a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts similarity index 98% rename from src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js rename to src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts index 2535642c27cc95..35b068a6910907 100644 --- a/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/__tests__/index_doc_records_stream.ts @@ -26,12 +26,12 @@ import { Progress } from '../../progress'; import { createIndexDocRecordsStream } from '../index_doc_records_stream'; import { createStubStats, createStubClient, createPersonDocRecords } from './stubs'; -const recordsToBulkBody = records => { +const recordsToBulkBody = (records: any[]) => { return records.reduce((acc, record) => { const { index, id, source } = record.value; return [...acc, { index: { _index: index, _id: id } }, source]; - }, []); + }, [] as any[]); }; describe('esArchiver: createIndexDocRecordsStream()', () => { diff --git a/src/es_archiver/lib/docs/__tests__/stubs.js b/src/es_archiver/lib/docs/__tests__/stubs.ts similarity index 74% rename from src/es_archiver/lib/docs/__tests__/stubs.js rename to src/es_archiver/lib/docs/__tests__/stubs.ts index 9ed48efa7d03ac..698d62e450cb46 100644 --- a/src/es_archiver/lib/docs/__tests__/stubs.js +++ b/src/es_archiver/lib/docs/__tests__/stubs.ts @@ -17,17 +17,22 @@ * under the License. */ +import { Client } from 'elasticsearch'; import sinon from 'sinon'; import Chance from 'chance'; import { times } from 'lodash'; + +import { Stats } from '../../stats'; + const chance = new Chance(); -export const createStubStats = () => ({ - indexedDoc: sinon.stub(), - archivedDoc: sinon.stub(), -}); +export const createStubStats = (): Stats => + ({ + indexedDoc: sinon.stub(), + archivedDoc: sinon.stub(), + } as any); -export const createPersonDocRecords = n => +export const createPersonDocRecords = (n: number) => times(n, () => ({ type: 'doc', value: { @@ -42,15 +47,21 @@ export const createPersonDocRecords = n => }, })); -export const createStubClient = (responses = []) => { - const createStubClientMethod = name => +type MockClient = Client & { + assertNoPendingResponses: () => void; +}; + +export const createStubClient = ( + responses: Array<(name: string, params: any) => any | Promise> = [] +): MockClient => { + const createStubClientMethod = (name: string) => sinon.spy(async params => { if (responses.length === 0) { throw new Error(`unexpected client.${name} call`); } const response = responses.shift(); - return await response(name, params); + return await response!(name, params); }); return { @@ -63,5 +74,5 @@ export const createStubClient = (responses = []) => { throw new Error(`There are ${responses.length} unsent responses.`); } }, - }; + } as any; }; diff --git a/src/es_archiver/lib/docs/generate_doc_records_stream.js b/src/es_archiver/lib/docs/generate_doc_records_stream.ts similarity index 80% rename from src/es_archiver/lib/docs/generate_doc_records_stream.js rename to src/es_archiver/lib/docs/generate_doc_records_stream.ts index be8b0351d95c8e..e255a0abc36c5f 100644 --- a/src/es_archiver/lib/docs/generate_doc_records_stream.js +++ b/src/es_archiver/lib/docs/generate_doc_records_stream.ts @@ -18,33 +18,36 @@ */ import { Transform } from 'stream'; +import { Client, SearchParams, SearchResponse } from 'elasticsearch'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; const SCROLL_SIZE = 1000; const SCROLL_TIMEOUT = '1m'; -export function createGenerateDocRecordsStream(client, stats, progress) { +export function createGenerateDocRecordsStream(client: Client, stats: Stats, progress: Progress) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(index, enc, callback) { try { - let remainingHits = null; - let resp = null; + let remainingHits = 0; + let resp: SearchResponse | null = null; while (!resp || remainingHits > 0) { if (!resp) { resp = await client.search({ - index: index, + index, scroll: SCROLL_TIMEOUT, size: SCROLL_SIZE, _source: true, - rest_total_hits_as_int: true, - }); + rest_total_hits_as_int: true, // not declared on SearchParams type + } as SearchParams); remainingHits = resp.hits.total; progress.addToTotal(remainingHits); } else { resp = await client.scroll({ - scrollId: resp._scroll_id, + scrollId: resp._scroll_id!, scroll: SCROLL_TIMEOUT, }); } @@ -68,7 +71,7 @@ export function createGenerateDocRecordsStream(client, stats, progress) { progress.addToComplete(resp.hits.hits.length); } - callback(null); + callback(undefined); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/docs/index.js b/src/es_archiver/lib/docs/index.ts similarity index 100% rename from src/es_archiver/lib/docs/index.js rename to src/es_archiver/lib/docs/index.ts diff --git a/src/es_archiver/lib/docs/index_doc_records_stream.js b/src/es_archiver/lib/docs/index_doc_records_stream.ts similarity index 86% rename from src/es_archiver/lib/docs/index_doc_records_stream.js rename to src/es_archiver/lib/docs/index_doc_records_stream.ts index 73fb75c52ff0a6..8236ae8adb6db7 100644 --- a/src/es_archiver/lib/docs/index_doc_records_stream.js +++ b/src/es_archiver/lib/docs/index_doc_records_stream.ts @@ -17,11 +17,14 @@ * under the License. */ +import { Client } from 'elasticsearch'; import { Writable } from 'stream'; +import { Stats } from '../stats'; +import { Progress } from '../progress'; -export function createIndexDocRecordsStream(client, stats, progress) { - async function indexDocs(docs) { - const body = []; +export function createIndexDocRecordsStream(client: Client, stats: Stats, progress: Progress) { + async function indexDocs(docs: any[]) { + const body: any[] = []; docs.forEach(doc => { stats.indexedDoc(doc.index); diff --git a/src/es_archiver/lib/index.js b/src/es_archiver/lib/index.ts similarity index 96% rename from src/es_archiver/lib/index.js rename to src/es_archiver/lib/index.ts index 246dd8169cd6b0..960d51e411859e 100644 --- a/src/es_archiver/lib/index.js +++ b/src/es_archiver/lib/index.ts @@ -30,7 +30,7 @@ export { export { createFilterRecordsStream } from './records'; -export { createStats } from './stats'; +export { createStats, Stats } from './stats'; export { isGzip, diff --git a/src/es_archiver/lib/indices/__tests__/create_index_stream.js b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts similarity index 76% rename from src/es_archiver/lib/indices/__tests__/create_index_stream.js rename to src/es_archiver/lib/indices/__tests__/create_index_stream.ts index 9e0f83c9f7eb96..c90497eded88ce 100644 --- a/src/es_archiver/lib/indices/__tests__/create_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/create_index_stream.ts @@ -34,10 +34,13 @@ import { createStubIndexRecord, createStubDocRecord, createStubClient, + createStubLogger, } from './stubs'; const chance = new Chance(); +const log = createStubLogger(); + describe('esArchiver: createCreateIndexStream()', () => { describe('defaults', () => { it('deletes existing indices, creates all', async () => { @@ -48,15 +51,15 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), ]); expect(stats.getTestSummary()).to.eql({ deletedIndex: 1, createdIndex: 2, }); - sinon.assert.callCount(client.indices.delete, 1); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 1); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('deletes existing aliases, creates all', async () => { @@ -67,14 +70,19 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('existing-index'), createStubIndexRecord('new-index'), ]), - createCreateIndexStream({ client, stats, log: { debug: () => {} } }), + createCreateIndexStream({ client, stats, log }), ]); - expect(client.indices.getAlias.calledOnce).to.be.ok(); - expect(client.indices.getAlias.args[0][0]).to.eql({ name: 'existing-index', ignore: [404] }); - expect(client.indices.delete.calledOnce).to.be.ok(); - expect(client.indices.delete.args[0][0]).to.eql({ index: ['actual-index'] }); - sinon.assert.callCount(client.indices.create, 3); // one failed create because of existing + expect((client.indices.getAlias as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.getAlias as sinon.SinonSpy).args[0][0]).to.eql({ + name: 'existing-index', + ignore: [404], + }); + expect((client.indices.delete as sinon.SinonSpy).calledOnce).to.be.ok(); + expect((client.indices.delete as sinon.SinonSpy).args[0][0]).to.eql({ + index: ['actual-index'], + }); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 3); // one failed create because of existing }); it('passes through "hit" records', async () => { @@ -86,7 +94,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubDocRecord('index', 1), createStubDocRecord('index', 2), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -101,11 +109,11 @@ describe('esArchiver: createCreateIndexStream()', () => { createStubIndexRecord('index', { foo: {} }), createStubDocRecord('index', 1), ]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); - sinon.assert.calledWith(client.indices.create, { + sinon.assert.calledWith(client.indices.create as sinon.SinonSpy, { method: 'PUT', index: 'index', body: { @@ -126,7 +134,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream([createStubIndexRecord('index'), ...randoms]), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -140,7 +148,7 @@ describe('esArchiver: createCreateIndexStream()', () => { const output = await createPromiseFromStreams([ createListStream(nonRecordValues), - createCreateIndexStream({ client, stats }), + createCreateIndexStream({ client, stats, log }), createConcatStream([]), ]); @@ -161,6 +169,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), ]); @@ -169,9 +178,12 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing - expect(client.indices.create.args[0][0]).to.have.property('index', 'new-index'); + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing + expect((client.indices.create as sinon.SinonSpy).args[0][0]).to.have.property( + 'index', + 'new-index' + ); }); it('filters documents for skipped indices', async () => { @@ -190,6 +202,7 @@ describe('esArchiver: createCreateIndexStream()', () => { createCreateIndexStream({ client, stats, + log, skipExisting: true, }), createConcatStream([]), @@ -199,8 +212,8 @@ describe('esArchiver: createCreateIndexStream()', () => { skippedIndex: 1, createdIndex: 1, }); - sinon.assert.callCount(client.indices.delete, 0); - sinon.assert.callCount(client.indices.create, 2); // one failed create because of existing + sinon.assert.callCount(client.indices.delete as sinon.SinonSpy, 0); + sinon.assert.callCount(client.indices.create as sinon.SinonSpy, 2); // one failed create because of existing expect(output).to.have.length(2); expect(output).to.eql([ diff --git a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts similarity index 66% rename from src/es_archiver/lib/indices/__tests__/delete_index_stream.js rename to src/es_archiver/lib/indices/__tests__/delete_index_stream.ts index 955d1fff8779ed..1c989ba158a29d 100644 --- a/src/es_archiver/lib/indices/__tests__/delete_index_stream.js +++ b/src/es_archiver/lib/indices/__tests__/delete_index_stream.ts @@ -23,7 +23,14 @@ import { createListStream, createPromiseFromStreams } from '../../../../legacy/u import { createDeleteIndexStream } from '../delete_index_stream'; -import { createStubStats, createStubClient, createStubIndexRecord } from './stubs'; +import { + createStubStats, + createStubClient, + createStubIndexRecord, + createStubLogger, +} from './stubs'; + +const log = createStubLogger(); describe('esArchiver: createDeleteIndexStream()', () => { it('deletes the index without checking if it exists', async () => { @@ -32,13 +39,13 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.notCalled(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.notCalled(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('reports the delete when the index existed', async () => { @@ -47,12 +54,12 @@ describe('esArchiver: createDeleteIndexStream()', () => { await createPromiseFromStreams([ createListStream([createStubIndexRecord('index1')]), - createDeleteIndexStream(client, stats), + createDeleteIndexStream(client, stats, log, []), ]); - sinon.assert.calledOnce(stats.deletedIndex); - sinon.assert.notCalled(client.indices.create); - sinon.assert.calledOnce(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.calledOnce(stats.deletedIndex as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); }); diff --git a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js rename to src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts index 3523e9e82b153f..7a3712ca1a336f 100644 --- a/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/__tests__/generate_index_records_stream.ts @@ -45,10 +45,10 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { archivedIndex: 4, }); - sinon.assert.callCount(client.indices.get, 4); - sinon.assert.notCalled(client.indices.create); - sinon.assert.notCalled(client.indices.delete); - sinon.assert.notCalled(client.indices.exists); + sinon.assert.callCount(client.indices.get as sinon.SinonSpy, 4); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.delete as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); it('filters index metadata from settings', async () => { @@ -60,9 +60,9 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { createGenerateIndexRecordsStream(client, stats), ]); - const params = client.indices.get.args[0][0]; + const params = (client.indices.get as sinon.SinonSpy).args[0][0]; expect(params).to.have.property('filterPath'); - const filters = params.filterPath; + const filters: string[] = params.filterPath; expect(filters.some(path => path.includes('index.creation_date'))).to.be(true); expect(filters.some(path => path.includes('index.uuid'))).to.be(true); expect(filters.some(path => path.includes('index.version'))).to.be(true); @@ -73,7 +73,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const stats = createStubStats(); const client = createStubClient(['index1', 'index2', 'index3']); - const indexRecords = await createPromiseFromStreams([ + const indexRecords = await createPromiseFromStreams([ createListStream(['index1', 'index2', 'index3']), createGenerateIndexRecordsStream(client, stats), createConcatStream([]), diff --git a/src/es_archiver/lib/indices/__tests__/stubs.js b/src/es_archiver/lib/indices/__tests__/stubs.js deleted file mode 100644 index 00649a06f9efef..00000000000000 --- a/src/es_archiver/lib/indices/__tests__/stubs.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; - -export const createStubStats = () => ({ - createdIndex: sinon.stub(), - createdAliases: sinon.stub(), - deletedIndex: sinon.stub(), - skippedIndex: sinon.stub(), - archivedIndex: sinon.stub(), - getTestSummary() { - const summary = {}; - Object.keys(this).forEach(key => { - if (this[key].callCount) { - summary[key] = this[key].callCount; - } - }); - return summary; - }, -}); - -export const createStubIndexRecord = (index, aliases = {}) => ({ - type: 'index', - value: { index, aliases }, -}); - -export const createStubDocRecord = (index, id) => ({ - type: 'doc', - value: { index, id }, -}); - -const createEsClientError = errorType => { - const err = new Error(`ES Client Error Stub "${errorType}"`); - err.body = { - error: { - type: errorType, - }, - }; - return err; -}; - -const indexAlias = (aliases, index) => Object.keys(aliases).find(k => aliases[k] === index); - -export const createStubClient = (existingIndices = [], aliases = {}) => ({ - indices: { - get: sinon.spy(async ({ index }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - - return { - [index]: { - mappings: {}, - settings: {}, - }, - }; - }), - existsAlias: sinon.spy(({ name }) => { - return Promise.resolve(aliases.hasOwnProperty(name)); - }), - getAlias: sinon.spy(async ({ index, name }) => { - if (index && existingIndices.indexOf(index) >= 0) { - const result = indexAlias(aliases, index); - return { [index]: { aliases: result ? { [result]: {} } : {} } }; - } - - if (name && aliases[name]) { - return { [aliases[name]]: { aliases: { [name]: {} } } }; - } - - return { status: 404 }; - }), - updateAliases: sinon.spy(async ({ body }) => { - body.actions.forEach(({ add: { index, alias } }) => { - if (!existingIndices.includes(index)) { - throw createEsClientError('index_not_found_exception'); - } - existingIndices.push({ index, alias }); - }); - - return { ok: true }; - }), - create: sinon.spy(async ({ index }) => { - if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { - throw createEsClientError('resource_already_exists_exception'); - } else { - existingIndices.push(index); - return { ok: true }; - } - }), - delete: sinon.spy(async ({ index }) => { - const indices = Array.isArray(index) ? index : [index]; - if (indices.every(ix => existingIndices.includes(ix))) { - // Delete aliases associated with our indices - indices.forEach(ix => { - const alias = Object.keys(aliases).find(k => aliases[k] === ix); - if (alias) { - delete aliases[alias]; - } - }); - indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); - return { ok: true }; - } else { - throw createEsClientError('index_not_found_exception'); - } - }), - exists: sinon.spy(async () => { - throw new Error('Do not use indices.exists(). React to errors instead.'); - }), - }, -}); diff --git a/src/es_archiver/lib/indices/__tests__/stubs.ts b/src/es_archiver/lib/indices/__tests__/stubs.ts new file mode 100644 index 00000000000000..3f4682299c38d8 --- /dev/null +++ b/src/es_archiver/lib/indices/__tests__/stubs.ts @@ -0,0 +1,154 @@ +/* + * 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 { Client } from 'elasticsearch'; +import sinon from 'sinon'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../../stats'; + +type StubStats = Stats & { + getTestSummary: () => Record; +}; + +export const createStubStats = (): StubStats => + ({ + createdIndex: sinon.stub(), + createdAliases: sinon.stub(), + deletedIndex: sinon.stub(), + skippedIndex: sinon.stub(), + archivedIndex: sinon.stub(), + getTestSummary() { + const summary: Record = {}; + Object.keys(this).forEach(key => { + if (this[key].callCount) { + summary[key] = this[key].callCount; + } + }); + return summary; + }, + } as any); + +export const createStubLogger = (): ToolingLog => + ({ + debug: sinon.stub(), + info: sinon.stub(), + success: sinon.stub(), + warning: sinon.stub(), + error: sinon.stub(), + } as any); + +export const createStubIndexRecord = (index: string, aliases = {}) => ({ + type: 'index', + value: { index, aliases }, +}); + +export const createStubDocRecord = (index: string, id: number) => ({ + type: 'doc', + value: { index, id }, +}); + +const createEsClientError = (errorType: string) => { + const err = new Error(`ES Client Error Stub "${errorType}"`); + (err as any).body = { + error: { + type: errorType, + }, + }; + return err; +}; + +const indexAlias = (aliases: Record, index: string) => + Object.keys(aliases).find(k => aliases[k] === index); + +type StubClient = Client; + +export const createStubClient = ( + existingIndices: string[] = [], + aliases: Record = {} +): StubClient => + ({ + indices: { + get: sinon.spy(async ({ index }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + + return { + [index]: { + mappings: {}, + settings: {}, + }, + }; + }), + existsAlias: sinon.spy(({ name }) => { + return Promise.resolve(aliases.hasOwnProperty(name)); + }), + getAlias: sinon.spy(async ({ index, name }) => { + if (index && existingIndices.indexOf(index) >= 0) { + const result = indexAlias(aliases, index); + return { [index]: { aliases: result ? { [result]: {} } : {} } }; + } + + if (name && aliases[name]) { + return { [aliases[name]]: { aliases: { [name]: {} } } }; + } + + return { status: 404 }; + }), + updateAliases: sinon.spy(async ({ body }) => { + body.actions.forEach( + ({ add: { index, alias } }: { add: { index: string; alias: string } }) => { + if (!existingIndices.includes(index)) { + throw createEsClientError('index_not_found_exception'); + } + existingIndices.push({ index, alias } as any); + } + ); + + return { ok: true }; + }), + create: sinon.spy(async ({ index }) => { + if (existingIndices.includes(index) || aliases.hasOwnProperty(index)) { + throw createEsClientError('resource_already_exists_exception'); + } else { + existingIndices.push(index); + return { ok: true }; + } + }), + delete: sinon.spy(async ({ index }) => { + const indices = Array.isArray(index) ? index : [index]; + if (indices.every(ix => existingIndices.includes(ix))) { + // Delete aliases associated with our indices + indices.forEach(ix => { + const alias = Object.keys(aliases).find(k => aliases[k] === ix); + if (alias) { + delete aliases[alias]; + } + }); + indices.forEach(ix => existingIndices.splice(existingIndices.indexOf(ix), 1)); + return { ok: true }; + } else { + throw createEsClientError('index_not_found_exception'); + } + }), + exists: sinon.spy(async () => { + throw new Error('Do not use indices.exists(). React to errors instead.'); + }), + }, + } as any); diff --git a/src/es_archiver/lib/indices/create_index_stream.js b/src/es_archiver/lib/indices/create_index_stream.ts similarity index 81% rename from src/es_archiver/lib/indices/create_index_stream.js rename to src/es_archiver/lib/indices/create_index_stream.ts index 8fe4bc568cd231..df9d3bb623ad65 100644 --- a/src/es_archiver/lib/indices/create_index_stream.js +++ b/src/es_archiver/lib/indices/create_index_stream.ts @@ -17,13 +17,36 @@ * under the License. */ -import { Transform } from 'stream'; - +import { Transform, Readable } from 'stream'; import { get, once } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; + +import { Stats } from '../stats'; import { deleteKibanaIndices } from './kibana_index'; import { deleteIndex } from './delete_index'; -export function createCreateIndexStream({ client, stats, skipExisting, log }) { +interface DocRecord { + value: { + index: string; + type: string; + settings: Record; + mappings: Record; + aliases: Record; + }; +} + +export function createCreateIndexStream({ + client, + stats, + skipExisting = false, + log, +}: { + client: Client; + stats: Stats; + skipExisting?: boolean; + log: ToolingLog; +}) { const skipDocsFromIndices = new Set(); // If we're trying to import Kibana index docs, we need to ensure that @@ -31,7 +54,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { // migrations. This only needs to be done once per archive load operation. const deleteKibanaIndicesOnce = once(deleteKibanaIndices); - async function handleDoc(stream, record) { + async function handleDoc(stream: Readable, record: DocRecord) { if (skipDocsFromIndices.has(record.value.index)) { return; } @@ -39,7 +62,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { stream.push(record); } - async function handleIndex(record) { + async function handleIndex(record: DocRecord) { const { index, settings, mappings, aliases } = record.value; const isKibana = index.startsWith('.kibana'); @@ -102,7 +125,7 @@ export function createCreateIndexStream({ client, stats, skipExisting, log }) { break; } - callback(null); + callback(); } catch (err) { callback(err); } diff --git a/src/es_archiver/lib/indices/delete_index.js b/src/es_archiver/lib/indices/delete_index.ts similarity index 76% rename from src/es_archiver/lib/indices/delete_index.js rename to src/es_archiver/lib/indices/delete_index.ts index 6f60d9533a36b3..e3fca587fbc3d3 100644 --- a/src/es_archiver/lib/indices/delete_index.js +++ b/src/es_archiver/lib/indices/delete_index.ts @@ -18,22 +18,34 @@ */ import { get } from 'lodash'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; // see https://github.com/elastic/elasticsearch/blob/99f88f15c5febbca2d13b5b5fda27b844153bf1a/server/src/main/java/org/elasticsearch/cluster/SnapshotsInProgress.java#L313-L319 const PENDING_SNAPSHOT_STATUSES = ['INIT', 'STARTED', 'WAITING']; -export async function deleteIndex(options) { +export async function deleteIndex(options: { + client: Client; + stats: Stats; + index: string; + log: ToolingLog; + retryIfSnapshottingCount?: number; +}): Promise { const { client, stats, index, log, retryIfSnapshottingCount = 10 } = options; const getIndicesToDelete = async () => { const aliasInfo = await client.indices.getAlias({ name: index, ignore: [404] }); - return aliasInfo.status === 404 ? index : Object.keys(aliasInfo); + return aliasInfo.status === 404 ? [index] : Object.keys(aliasInfo); }; try { const indicesToDelete = await getIndicesToDelete(); await client.indices.delete({ index: indicesToDelete }); - stats.deletedIndex(indicesToDelete); + for (let i = 0; i < indicesToDelete.length; i++) { + const indexToDelete = indicesToDelete[i]; + stats.deletedIndex(indexToDelete); + } } catch (error) { if (retryIfSnapshottingCount > 0 && isDeleteWhileSnapshotInProgressError(error)) { stats.waitingForInProgressSnapshot(index); @@ -56,7 +68,7 @@ export async function deleteIndex(options) { * @param {Error} error * @return {Boolean} */ -export function isDeleteWhileSnapshotInProgressError(error) { +export function isDeleteWhileSnapshotInProgressError(error: object) { return get(error, 'body.error.reason', '').startsWith( 'Cannot delete indices that are being snapshotted' ); @@ -65,13 +77,9 @@ export function isDeleteWhileSnapshotInProgressError(error) { /** * Wait for the any snapshot in any repository that is * snapshotting this index to complete. - * - * @param {EsClient} client - * @param {string} index the name of the index to look for - * @return {Promise} */ -export async function waitForSnapshotCompletion(client, index, log) { - const isSnapshotPending = async (repository, snapshot) => { +export async function waitForSnapshotCompletion(client: Client, index: string, log: ToolingLog) { + const isSnapshotPending = async (repository: string, snapshot: string) => { const { snapshots: [status], } = await client.snapshot.status({ @@ -83,7 +91,7 @@ export async function waitForSnapshotCompletion(client, index, log) { return PENDING_SNAPSHOT_STATUSES.includes(status.state); }; - const getInProgressSnapshots = async repository => { + const getInProgressSnapshots = async (repository: string) => { const { snapshots: inProgressSnapshots } = await client.snapshot.get({ repository, snapshot: '_current', @@ -91,9 +99,9 @@ export async function waitForSnapshotCompletion(client, index, log) { return inProgressSnapshots; }; - for (const repository of Object.keys(await client.snapshot.getRepository())) { + for (const repository of Object.keys(await client.snapshot.getRepository({} as any))) { const allInProgress = await getInProgressSnapshots(repository); - const found = allInProgress.find(s => s.indices.includes(index)); + const found = allInProgress.find((s: any) => s.indices.includes(index)); if (!found) { continue; diff --git a/src/es_archiver/lib/indices/delete_index_stream.js b/src/es_archiver/lib/indices/delete_index_stream.ts similarity index 86% rename from src/es_archiver/lib/indices/delete_index_stream.js rename to src/es_archiver/lib/indices/delete_index_stream.ts index 31a49ed30a124d..b4e1e530e1f84e 100644 --- a/src/es_archiver/lib/indices/delete_index_stream.js +++ b/src/es_archiver/lib/indices/delete_index_stream.ts @@ -18,11 +18,19 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; import { cleanKibanaIndices } from './kibana_index'; -export function createDeleteIndexStream(client, stats, log, kibanaPluginIds) { +export function createDeleteIndexStream( + client: Client, + stats: Stats, + log: ToolingLog, + kibanaPluginIds: string[] +) { return new Transform({ readableObjectMode: true, writableObjectMode: true, diff --git a/src/es_archiver/lib/indices/generate_index_records_stream.js b/src/es_archiver/lib/indices/generate_index_records_stream.ts similarity index 89% rename from src/es_archiver/lib/indices/generate_index_records_stream.js rename to src/es_archiver/lib/indices/generate_index_records_stream.ts index 1d1a44aa634c24..b4b98f8ae262c7 100644 --- a/src/es_archiver/lib/indices/generate_index_records_stream.js +++ b/src/es_archiver/lib/indices/generate_index_records_stream.ts @@ -18,14 +18,16 @@ */ import { Transform } from 'stream'; +import { Client } from 'elasticsearch'; +import { Stats } from '../stats'; -export function createGenerateIndexRecordsStream(client, stats) { +export function createGenerateIndexRecordsStream(client: Client, stats: Stats) { return new Transform({ writableObjectMode: true, readableObjectMode: true, async transform(indexOrAlias, enc, callback) { try { - const resp = await client.indices.get({ + const resp = (await client.indices.get({ index: indexOrAlias, filterPath: [ '*.settings', @@ -36,7 +38,7 @@ export function createGenerateIndexRecordsStream(client, stats) { '-*.settings.index.version', '-*.settings.index.provided_name', ], - }); + })) as Record; for (const [index, { settings, mappings }] of Object.entries(resp)) { const { diff --git a/src/es_archiver/lib/indices/index.js b/src/es_archiver/lib/indices/index.ts similarity index 100% rename from src/es_archiver/lib/indices/index.js rename to src/es_archiver/lib/indices/index.ts diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.ts similarity index 70% rename from src/es_archiver/lib/indices/kibana_index.js rename to src/es_archiver/lib/indices/kibana_index.ts index 744132bdcef69e..de67ba7c4e31ec 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.ts @@ -17,29 +17,34 @@ * under the License. */ -import _ from 'lodash'; +import { get } from 'lodash'; import fs from 'fs'; -import path from 'path'; +import Path from 'path'; import { promisify } from 'util'; import { toArray } from 'rxjs/operators'; +import { Client, CreateDocumentParams } from 'elasticsearch'; +import { ToolingLog } from '@kbn/dev-utils'; +import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; -import { collectUiExports } from '../../../legacy/ui/ui_exports'; import { KibanaMigrator } from '../../../core/server/saved_objects/migrations'; import { SavedObjectsSchema } from '../../../core/server/saved_objects'; +// @ts-ignore +import { collectUiExports } from '../../../legacy/ui/ui_exports'; +// @ts-ignore import { findPluginSpecs } from '../../../legacy/plugin_discovery'; /** * Load the uiExports for a Kibana instance, only load uiExports from xpack if * it is enabled in the Kibana server. */ -const getUiExports = async kibanaPluginIds => { +const getUiExports = async (kibanaPluginIds: string[]) => { const xpackEnabled = kibanaPluginIds.includes('xpack_main'); const { spec$ } = await findPluginSpecs({ plugins: { - scanDirs: [path.resolve(__dirname, '../../../legacy/core_plugins')], - paths: xpackEnabled ? [path.resolve(__dirname, '../../../../x-pack')] : [], + scanDirs: [Path.resolve(__dirname, '../../../legacy/core_plugins')], + paths: xpackEnabled ? [Path.resolve(__dirname, '../../../../x-pack')] : [], }, }); @@ -50,7 +55,15 @@ const getUiExports = async kibanaPluginIds => { /** * Deletes all indices that start with `.kibana` */ -export async function deleteKibanaIndices({ client, stats, log }) { +export async function deleteKibanaIndices({ + client, + stats, + log, +}: { + client: Client; + stats: Stats; + log: ToolingLog; +}) { const indexNames = await fetchKibanaIndices(client); if (!indexNames.length) { return; @@ -76,37 +89,52 @@ export async function deleteKibanaIndices({ client, stats, log }) { * builds up an object that implements just enough of the kbnMigrations interface * as is required by migrations. */ -export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { +export async function migrateKibanaIndex({ + client, + log, + kibanaPluginIds, +}: { + client: Client; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { const uiExports = await getUiExports(kibanaPluginIds); const kibanaVersion = await loadKibanaVersion(); - const config = { + const config: Record = { 'xpack.task_manager.index': '.kibana_task_manager', }; + const logger = { + trace: log.verbose.bind(log), + debug: log.debug.bind(log), + info: log.info.bind(log), + warn: log.warning.bind(log), + error: log.error.bind(log), + fatal: log.error.bind(log), + log: (entry: any) => log.info(entry.message), + get: () => logger, + }; + const migratorOptions = { - config: { get: path => config[path] }, + config: { get: (path: string) => config[path] } as any, savedObjectsConfig: { scrollDuration: '5m', batchSize: 100, pollInterval: 100, + skip: false, }, kibanaConfig: { index: '.kibana', - }, - logger: { - trace: log.verbose.bind(log), - debug: log.debug.bind(log), - info: log.info.bind(log), - warn: log.warning.bind(log), - error: log.error.bind(log), - }, - version: kibanaVersion, + } as any, + logger, + kibanaVersion, savedObjectSchemas: new SavedObjectsSchema(uiExports.savedObjectSchemas), savedObjectMappings: uiExports.savedObjectMappings, savedObjectMigrations: uiExports.savedObjectMigrations, savedObjectValidations: uiExports.savedObjectValidations, - callCluster: (path, ...args) => _.get(client, path).call(client, ...args), + callCluster: (path: string, ...args: any[]) => + (get(client, path) as Function).call(client, ...args), }; return await new KibanaMigrator(migratorOptions).runMigrations(); @@ -114,8 +142,8 @@ export async function migrateKibanaIndex({ client, log, kibanaPluginIds }) { async function loadKibanaVersion() { const readFile = promisify(fs.readFile); - const packageJson = await readFile(path.join(__dirname, '../../../../package.json')); - return JSON.parse(packageJson).version; + const packageJson = await readFile(Path.join(__dirname, '../../../../package.json')); + return JSON.parse(packageJson.toString('utf-8')).version; } /** @@ -123,16 +151,24 @@ async function loadKibanaVersion() { * .kibana, .kibana_1, .kibana_323, etc. This finds all indices starting * with .kibana, then filters out any that aren't actually Kibana's core * index (e.g. we don't want to remove .kibana_task_manager or the like). - * - * @param {string} index */ -async function fetchKibanaIndices(client) { +async function fetchKibanaIndices(client: Client) { const kibanaIndices = await client.cat.indices({ index: '.kibana*', format: 'json' }); - const isKibanaIndex = index => /^\.kibana(:?_\d*)?$/.test(index); - return kibanaIndices.map(x => x.index).filter(isKibanaIndex); + const isKibanaIndex = (index: string) => /^\.kibana(:?_\d*)?$/.test(index); + return kibanaIndices.map((x: { index: string }) => x.index).filter(isKibanaIndex); } -export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds }) { +export async function cleanKibanaIndices({ + client, + stats, + log, + kibanaPluginIds, +}: { + client: Client; + stats: Stats; + log: ToolingLog; + kibanaPluginIds: string[]; +}) { if (!kibanaPluginIds.includes('spaces')) { return await deleteKibanaIndices({ client, @@ -178,7 +214,7 @@ export async function cleanKibanaIndices({ client, stats, log, kibanaPluginIds } stats.deletedIndex('.kibana'); } -export async function createDefaultSpace({ index, client }) { +export async function createDefaultSpace({ index, client }: { index: string; client: Client }) { await client.create({ index, id: 'space:default', @@ -193,5 +229,5 @@ export async function createDefaultSpace({ index, client }) { _reserved: true, }, }, - }); + } as CreateDocumentParams); } diff --git a/src/es_archiver/lib/records/__tests__/filter_records_stream.js b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts similarity index 97% rename from src/es_archiver/lib/records/__tests__/filter_records_stream.js rename to src/es_archiver/lib/records/__tests__/filter_records_stream.ts index fd35575ca59bae..d5830478decbae 100644 --- a/src/es_archiver/lib/records/__tests__/filter_records_stream.js +++ b/src/es_archiver/lib/records/__tests__/filter_records_stream.ts @@ -51,7 +51,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { it('produces record values that have a matching type', async () => { const type1 = chance.word({ length: 5 }); - const output = await createPromiseFromStreams([ + const output = await createPromiseFromStreams([ createListStream([ { type: type1, value: {} }, { type: type1, value: {} }, diff --git a/src/es_archiver/lib/records/filter_records_stream.js b/src/es_archiver/lib/records/filter_records_stream.ts similarity index 91% rename from src/es_archiver/lib/records/filter_records_stream.js rename to src/es_archiver/lib/records/filter_records_stream.ts index 5a835ffe8e84d5..191cbd3b921e3b 100644 --- a/src/es_archiver/lib/records/filter_records_stream.js +++ b/src/es_archiver/lib/records/filter_records_stream.ts @@ -19,14 +19,14 @@ import { Transform } from 'stream'; -export function createFilterRecordsStream(type) { +export function createFilterRecordsStream(type: string) { return new Transform({ writableObjectMode: true, readableObjectMode: true, transform(record, enc, callback) { if (record && record.type === type) { - callback(null, record); + callback(undefined, record); } else { callback(); } diff --git a/src/es_archiver/lib/records/index.js b/src/es_archiver/lib/records/index.ts similarity index 100% rename from src/es_archiver/lib/records/index.js rename to src/es_archiver/lib/records/index.ts diff --git a/src/es_archiver/lib/stats.ts b/src/es_archiver/lib/stats.ts index 5f73304abf9a8b..c69b764fc72908 100644 --- a/src/es_archiver/lib/stats.ts +++ b/src/es_archiver/lib/stats.ts @@ -37,6 +37,8 @@ export interface IndexStats { }; } +export type Stats = ReturnType; + export function createStats(name: string, log: ToolingLog) { const info = (msg: string, ...args: any[]) => log.info(`[${name}] ${msg}`, ...args); const debug = (msg: string, ...args: any[]) => log.debug(`[${name}] ${msg}`, ...args); diff --git a/src/legacy/utils/index.d.ts b/src/legacy/utils/index.d.ts index a57caad1d34bf8..c294c79542bbe9 100644 --- a/src/legacy/utils/index.d.ts +++ b/src/legacy/utils/index.d.ts @@ -18,3 +18,16 @@ */ export function unset(object: object, rawPath: string): void; + +export { + concatStreamProviders, + createConcatStream, + createFilterStream, + createIntersperseStream, + createListStream, + createMapStream, + createPromiseFromStreams, + createReduceStream, + createReplaceStream, + createSplitStream, +} from './streams'; diff --git a/src/legacy/utils/streams/index.d.ts b/src/legacy/utils/streams/index.d.ts index b8d4c67050b2da..5ef39b292c6858 100644 --- a/src/legacy/utils/streams/index.d.ts +++ b/src/legacy/utils/streams/index.d.ts @@ -20,17 +20,17 @@ import { Readable, Transform, Writable, TransformOptions } from 'stream'; export function concatStreamProviders( - sourceProviders: Readable[], + sourceProviders: Array<() => Readable>, options: TransformOptions ): Transform; export function createIntersperseStream(intersperseChunk: string | Buffer): Transform; export function createSplitStream(splitChunk: T): Transform; -export function createListStream(items: any[]): Readable; +export function createListStream(items: any | any[]): Readable; export function createReduceStream(reducer: (value: any, chunk: T, enc: string) => T): Transform; export function createPromiseFromStreams([first, ...rest]: [Readable, ...Writable[]]): Promise< T >; -export function createConcatStream(initial: any): Transform; +export function createConcatStream(initial?: any): Transform; export function createMapStream(fn: (value: T, i: number) => void): Transform; export function createReplaceStream(toReplace: string, replacement: string | Buffer): Transform; export function createFilterStream(fn: (obj: T) => boolean): Transform; From 3d96e4c95bdc7ce2401b66fc1c816d826496382a Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Mon, 3 Feb 2020 13:24:38 +0100 Subject: [PATCH 35/62] [SIEM] Fix Welcome to SIEM typo (#56582) --- x-pack/legacy/plugins/siem/public/pages/common/translations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts index 3e203383756163..072aee50d5136b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/common/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/common/translations.ts @@ -12,7 +12,7 @@ export const EMPTY_TITLE = i18n.translate('xpack.siem.pages.common.emptyTitle', export const EMPTY_MESSAGE = i18n.translate('xpack.siem.pages.common.emptyMessage', { defaultMessage: - 'To begin using security information and event management, you’ll need to begin adding SIEM-related data to Kibana by installing and configuring our data shippers, called Beats. Let’s do that now!', + 'To begin using security information and event management (SIEM), you’ll need to add SIEM-related data, in Elastic Common Schema (ECS) format, to the Elastic Stack. An easy way to get started is by installing and configuring our data shippers, called Beats. Let’s do that now!', }); export const EMPTY_ACTION_PRIMARY = i18n.translate('xpack.siem.pages.common.emptyActionPrimary', { From 598968f20f1b3b09429dd6eac079b010913f0dee Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 3 Feb 2020 13:33:19 +0100 Subject: [PATCH 36/62] fix duplicate header warning (#56491) logged when attaching 2+ headers due to excessive check Co-authored-by: Elastic Machine --- src/core/server/http/integration_tests/lifecycle.test.ts | 1 + src/core/server/http/lifecycle/on_pre_response.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index b16352838fad11..6dc7ece1359df7 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -721,6 +721,7 @@ describe('Auth', () => { res.ok({ headers: { 'www-authenticate': 'from handler', + 'another-header': 'yet another header', }, }) ); diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts index 45d7478df98051..50d3d7b47bf8d6 100644 --- a/src/core/server/http/lifecycle/on_pre_response.ts +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -120,8 +120,8 @@ export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Lo ...(result.headers as any), // hapi types don't specify string[] as valid value }; } else { + findHeadersIntersection(response.headers, result.headers, log); for (const [headerName, headerValue] of Object.entries(result.headers)) { - findHeadersIntersection(response.headers, result.headers, log); response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value } } From 8def60e1dac63ba7d7900bbffca6e1519ff1dfb7 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Mon, 3 Feb 2020 14:43:10 +0100 Subject: [PATCH 37/62] Unify Security and EncryptedSavedObjects public contract names according to NP migration guide. (#56597) --- .../plugins/alerting/server/alerts_client.ts | 6 +++--- .../alerting/server/alerts_client_factory.ts | 6 +++--- x-pack/legacy/plugins/alerting/server/shim.ts | 18 +++++++++--------- .../server/task_runner/task_runner_factory.ts | 4 ++-- .../plugins/encrypted_saved_objects/index.ts | 4 ++-- x-pack/legacy/plugins/reporting/index.ts | 2 +- .../legacy/plugins/reporting/server/plugin.ts | 2 +- x-pack/legacy/plugins/siem/server/plugin.ts | 4 ++-- .../actions/server/lib/action_executor.ts | 4 ++-- .../actions/server/lib/task_runner_factory.ts | 4 ++-- x-pack/plugins/actions/server/plugin.ts | 8 ++++---- x-pack/plugins/case/server/plugin.ts | 2 +- x-pack/plugins/case/server/services/index.ts | 5 +---- .../encrypted_saved_objects/server/index.ts | 2 +- .../encrypted_saved_objects/server/mocks.ts | 6 +++--- .../encrypted_saved_objects/server/plugin.ts | 6 +++--- x-pack/plugins/security/server/index.ts | 6 +++--- x-pack/plugins/security/server/mocks.ts | 4 ++-- x-pack/plugins/security/server/plugin.ts | 4 ++-- .../lib/spaces_client/spaces_client.test.ts | 8 ++++---- .../server/lib/spaces_client/spaces_client.ts | 6 +++--- x-pack/plugins/spaces/server/plugin.ts | 2 +- .../server/spaces_service/spaces_service.ts | 2 +- .../common/fixtures/plugins/aad/index.ts | 5 +++-- .../plugins/encrypted_saved_objects/index.ts | 9 +++++---- 25 files changed, 64 insertions(+), 65 deletions(-) diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client.ts b/x-pack/legacy/plugins/alerting/server/alerts_client.ts index a6ba936b76570e..1346d403edda1a 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client.ts @@ -29,7 +29,7 @@ import { CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult, InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult, } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; type NormalizedAlertAction = Omit; @@ -45,7 +45,7 @@ interface ConstructorOptions { taskManager: TaskManagerStartContract; savedObjectsClient: SavedObjectsClientContract; alertTypeRegistry: AlertTypeRegistry; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceId?: string; namespace?: string; getUserName: () => Promise; @@ -120,7 +120,7 @@ export class AlertsClient { private readonly invalidateAPIKey: ( params: InvalidateAPIKeyParams ) => Promise; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor({ alertTypeRegistry, diff --git a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts index eab1cc3ce627b4..de789fba0ac388 100644 --- a/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/alerts_client_factory.ts @@ -11,7 +11,7 @@ import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types'; import { SecurityPluginStartContract } from './shim'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; import { InvalidateAPIKeyParams } from '../../../../plugins/security/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../plugins/encrypted_saved_objects/server'; import { TaskManagerStartContract } from '../../../../plugins/task_manager/server'; export interface ConstructorOpts { @@ -21,7 +21,7 @@ export interface ConstructorOpts { securityPluginSetup?: SecurityPluginStartContract; getSpaceId: (request: Hapi.Request) => string | undefined; spaceIdToNamespace: SpaceIdToNamespaceFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; } export class AlertsClientFactory { @@ -31,7 +31,7 @@ export class AlertsClientFactory { private readonly securityPluginSetup?: SecurityPluginStartContract; private readonly getSpaceId: (request: Hapi.Request) => string | undefined; private readonly spaceIdToNamespace: SpaceIdToNamespaceFunction; - private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; constructor(options: ConstructorOpts) { this.logger = options.logger; diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index 80d01ea722926f..bc8b0eb863634c 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -15,10 +15,10 @@ import { getTaskManagerSetup, getTaskManagerStart } from '../../task_manager/ser import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import KbnServer from '../../../../../src/legacy/server/kbn_server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { CoreSetup, LoggerFactory, @@ -44,8 +44,8 @@ export interface Server extends Legacy.Server { /** * Shim what we're thinking setup and start contracts will look like */ -export type SecurityPluginSetupContract = Pick; -export type SecurityPluginStartContract = Pick; +export type SecurityPluginSetupContract = Pick; +export type SecurityPluginStartContract = Pick; export type XPackMainPluginSetupContract = Pick; /** @@ -71,14 +71,14 @@ export interface AlertingPluginsSetup { taskManager: TaskManagerSetupContract; actions: ActionsPluginSetupContract; xpack_main: XPackMainPluginSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; } export interface AlertingPluginsStart { actions: ActionsPluginStartContract; security?: SecurityPluginStartContract; spaces: () => SpacesPluginStartContract | undefined; - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } @@ -120,7 +120,7 @@ export function shim( actions: newPlatform.setup.plugins.actions as ActionsPluginSetupContract, xpack_main: server.plugins.xpack_main, encryptedSavedObjects: newPlatform.setup.plugins - .encryptedSavedObjects as EncryptedSavedObjectsSetupContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup, licensing: newPlatform.setup.plugins.licensing as LicensingPluginSetup, }; @@ -131,7 +131,7 @@ export function shim( // initializes after this function is called spaces: () => server.plugins.spaces, encryptedSavedObjects: newPlatform.start.plugins - .encryptedSavedObjects as EncryptedSavedObjectsStartContract, + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart, taskManager: getTaskManagerStart(server)!, }; diff --git a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts index 67fef33b69c6dd..d2ecfb64c8a813 100644 --- a/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts +++ b/x-pack/legacy/plugins/alerting/server/task_runner/task_runner_factory.ts @@ -5,7 +5,7 @@ */ import { Logger } from '../../../../../../src/core/server'; import { RunContext } from '../../../../../plugins/task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../plugins/encrypted_saved_objects/server'; import { PluginStartContract as ActionsPluginStartContract } from '../../../../../plugins/actions/server'; import { AlertType, @@ -19,7 +19,7 @@ export interface TaskRunnerContext { logger: Logger; getServices: GetServicesFunction; executeAction: ActionsPluginStartContract['execute']; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; } diff --git a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts index 69058a7a33f59f..ce343dba006cfc 100644 --- a/x-pack/legacy/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/legacy/plugins/encrypted_saved_objects/index.ts @@ -6,7 +6,7 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; -import { PluginSetupContract } from '../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../../plugins/encrypted_saved_objects/server'; // @ts-ignore import { AuditLogger } from '../../server/lib/audit_logger'; @@ -29,7 +29,7 @@ export const encryptedSavedObjects = (kibana: { init(server: Legacy.Server) { const encryptedSavedObjectsPlugin = (server.newPlatform.setup.plugins - .encryptedSavedObjects as unknown) as PluginSetupContract; + .encryptedSavedObjects as unknown) as EncryptedSavedObjectsPluginSetup; if (!encryptedSavedObjectsPlugin) { throw new Error('New Platform XPack EncryptedSavedObjects plugin is not available.'); } diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index d2a68e309a4b37..966e4ff209ad68 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -9,7 +9,7 @@ import { Legacy } from 'kibana'; import { IUiSettingsClient } from 'kibana/server'; import { resolve } from 'path'; import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../plugins/security/server'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; import { config as reportingConfig } from './config'; import { LegacySetup, ReportingPlugin, reportingPluginFactory } from './server/plugin'; diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts index a2938d442f7df0..e618d23e8ed1f9 100644 --- a/x-pack/legacy/plugins/reporting/server/plugin.ts +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -14,7 +14,7 @@ import { } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; // @ts-ignore import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 96eef2f44e5a06..94314367be59cf 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, PluginInitializerContext, Logger } from 'src/core/server'; -import { PluginSetupContract as SecurityPlugin } from '../../../../plugins/security/server'; +import { SecurityPluginSetup } from '../../../../plugins/security/server'; import { PluginSetupContract as FeaturesSetupContract } from '../../../../plugins/features/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -17,7 +17,7 @@ import { ruleStatusSavedObjectType, } from './saved_objects'; -export type SiemPluginSecurity = Pick; +export type SiemPluginSecurity = Pick; export interface PluginsSetup { features: FeaturesSetupContract; diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index be6916a74fe88d..03a892a42792e2 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -12,7 +12,7 @@ import { GetServicesFunction, RawAction, } from '../types'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../plugin'; import { IEvent, IEventLogger } from '../../../event_log/server'; @@ -21,7 +21,7 @@ export interface ActionExecutorContext { logger: Logger; spaces?: SpacesServiceSetup; getServices: GetServicesFunction; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; } diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index c3e89e0c16efcc..c78b43f4ef3ba3 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -8,12 +8,12 @@ import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; import { Logger, CoreStart } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; -import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { ActionTaskParams, GetBasePathFunction, SpaceIdToNamespaceFunction } from '../types'; export interface TaskRunnerContext { logger: Logger; - encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract; + encryptedSavedObjectsPlugin: EncryptedSavedObjectsPluginStart; spaceIdToNamespace: SpaceIdToNamespaceFunction; getBasePath: GetBasePathFunction; getScopedSavedObjectsClient: CoreStart['savedObjects']['getScopedClient']; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index cb0e3347541fd7..dab09fc455ecf6 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -20,8 +20,8 @@ import { } from '../../../../src/core/server'; import { - PluginSetupContract as EncryptedSavedObjectsSetupContract, - PluginStartContract as EncryptedSavedObjectsStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../encrypted_saved_objects/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { LicensingPluginSetup } from '../../licensing/server'; @@ -67,13 +67,13 @@ export interface PluginStartContract { export interface ActionsPluginsSetup { taskManager: TaskManagerSetupContract; - encryptedSavedObjects: EncryptedSavedObjectsSetupContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; licensing: LicensingPluginSetup; spaces?: SpacesPluginSetup; event_log: IEventLogService; } export interface ActionsPluginsStart { - encryptedSavedObjects: EncryptedSavedObjectsStartContract; + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; taskManager: TaskManagerStartContract; } diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index c52461cade0586..37d087433a2ed1 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -9,7 +9,7 @@ import { CoreSetup, Logger, PluginInitializerContext } from 'kibana/server'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; import { CaseService } from './services'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; function createConfig$(context: PluginInitializerContext) { return context.config.create().pipe(map(config => config)); diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 684d905a5c71fe..531d5fa5b87e5b 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -21,10 +21,7 @@ import { UpdatedCaseType, UpdatedCommentType, } from '../routes/api/types'; -import { - AuthenticatedUser, - PluginSetupContract as SecurityPluginSetup, -} from '../../../security/server'; +import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server'; interface ClientArgs { client: SavedObjectsClientContract; diff --git a/x-pack/plugins/encrypted_saved_objects/server/index.ts b/x-pack/plugins/encrypted_saved_objects/server/index.ts index 5e6edb95ec37a6..3b4b91de355c74 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/index.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/index.ts @@ -9,7 +9,7 @@ import { ConfigSchema } from './config'; import { Plugin } from './plugin'; export { EncryptedSavedObjectTypeRegistration, EncryptionError } from './crypto'; -export { PluginSetupContract, PluginStartContract } from './plugin'; +export { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts index 7f53f47760f129..13d7127db78353 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/mocks.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/mocks.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract, PluginStartContract } from './plugin'; +import { EncryptedSavedObjectsPluginSetup, EncryptedSavedObjectsPluginStart } from './plugin'; function createEncryptedSavedObjectsSetupMock() { return { registerType: jest.fn(), __legacyCompat: { registerLegacyAPI: jest.fn() }, usingEphemeralEncryptionKey: true, - } as jest.Mocked; + } as jest.Mocked; } function createEncryptedSavedObjectsStartMock() { return { isEncryptionError: jest.fn(), getDecryptedAsInternalUser: jest.fn(), - } as jest.Mocked; + } as jest.Mocked; } export const encryptedSavedObjectsMock = { diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index d9185251ca4664..a0218c51c2723c 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -20,13 +20,13 @@ import { import { EncryptedSavedObjectsAuditLogger } from './audit'; import { SavedObjectsSetup, setupSavedObjects } from './saved_objects'; -export interface PluginSetupContract { +export interface EncryptedSavedObjectsPluginSetup { registerType: (typeRegistration: EncryptedSavedObjectTypeRegistration) => void; __legacyCompat: { registerLegacyAPI: (legacyAPI: LegacyAPI) => void }; usingEphemeralEncryptionKey: boolean; } -export interface PluginStartContract extends SavedObjectsSetup { +export interface EncryptedSavedObjectsPluginStart extends SavedObjectsSetup { isEncryptionError: (error: Error) => boolean; } @@ -59,7 +59,7 @@ export class Plugin { this.logger = this.initializerContext.logger.get(); } - public async setup(core: CoreSetup): Promise { + public async setup(core: CoreSetup): Promise { const { config, usingEphemeralEncryptionKey } = await createConfig$(this.initializerContext) .pipe(first()) .toPromise(); diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index 17e49b8cf40d30..c0e86b289fe545 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -12,7 +12,7 @@ import { RecursiveReadonly, } from '../../../../src/core/server'; import { ConfigSchema } from './config'; -import { Plugin, PluginSetupContract, PluginSetupDependencies } from './plugin'; +import { Plugin, SecurityPluginSetup, PluginSetupDependencies } from './plugin'; // These exports are part of public Security plugin contract, any change in signature of exported // functions or removal of exports should be considered as a breaking change. @@ -24,7 +24,7 @@ export { InvalidateAPIKeyParams, InvalidateAPIKeyResult, } from './authentication'; -export { PluginSetupContract }; +export { SecurityPluginSetup }; export { AuthenticatedUser } from '../common/model'; export const config: PluginConfigDescriptor> = { @@ -35,7 +35,7 @@ export const config: PluginConfigDescriptor> = { ], }; export const plugin: PluginInitializer< - RecursiveReadonly, + RecursiveReadonly, void, PluginSetupDependencies > = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext); diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index d5c08d5ab1ab94..ababf12c2be606 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract } from './plugin'; +import { SecurityPluginSetup } from './plugin'; import { authenticationMock } from './authentication/index.mock'; import { authorizationMock } from './authorization/index.mock'; @@ -19,7 +19,7 @@ function createSetupMock() { mode: mockAuthz.mode, }, registerSpacesService: jest.fn(), - __legacyCompat: {} as PluginSetupContract['__legacyCompat'], + __legacyCompat: {} as SecurityPluginSetup['__legacyCompat'], }; } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index ce682d8b30eb76..57644182347398 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -49,7 +49,7 @@ export interface LegacyAPI { /** * Describes public Security plugin contract returned at the `setup` stage. */ -export interface PluginSetupContract { +export interface SecurityPluginSetup { authc: Authentication; authz: Pick; @@ -166,7 +166,7 @@ export class Plugin { csp: core.http.csp, }); - return deepFreeze({ + return deepFreeze({ authc, authz: { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 24a994e836e87d..74e75fb8f12c79 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginSetupContract as SecuritySetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { SpacesClient } from './spaces_client'; import { ConfigType, ConfigSchema } from '../../config'; import { GetSpacePurpose } from '../../../common/model/types'; @@ -224,17 +224,17 @@ describe('#getAll', () => { [ { purpose: undefined, - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'any', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.login, }, { purpose: 'copySavedObjectsIntoSpace', - expectedPrivilege: (mockAuthorization: SecuritySetupContract['authz']) => + expectedPrivilege: (mockAuthorization: SecurityPluginSetup['authz']) => mockAuthorization.actions.ui.get('savedObjectsManagement', 'copyIntoSpace'), }, ].forEach(scenario => { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index f964ae7d7ac32d..22c34c03368e36 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -6,7 +6,7 @@ import Boom from 'boom'; import { omit } from 'lodash'; import { KibanaRequest } from 'src/core/server'; -import { PluginSetupContract as SecurityPluginSetupContract } from '../../../../security/server'; +import { SecurityPluginSetup } from '../../../../security/server'; import { isReservedSpace } from '../../../common/is_reserved_space'; import { Space } from '../../../common/model/space'; import { SpacesAuditLogger } from '../audit_logger'; @@ -17,7 +17,7 @@ const SUPPORTED_GET_SPACE_PURPOSES: GetSpacePurpose[] = ['any', 'copySavedObject const PURPOSE_PRIVILEGE_MAP: Record< GetSpacePurpose, - (authorization: SecurityPluginSetupContract['authz']) => string + (authorization: SecurityPluginSetup['authz']) => string > = { any: authorization => authorization.actions.login, copySavedObjectsIntoSpace: authorization => @@ -28,7 +28,7 @@ export class SpacesClient { constructor( private readonly auditLogger: SpacesAuditLogger, private readonly debugLogger: (message: string) => void, - private readonly authorization: SecurityPluginSetupContract['authz'] | null, + private readonly authorization: SecurityPluginSetup['authz'] | null, private readonly callWithRequestSavedObjectRepository: any, private readonly config: ConfigType, private readonly internalSavedObjectRepository: any, diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index b8ef81c05f7aa4..52ff7eaee3d68c 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -14,7 +14,7 @@ import { PluginInitializerContext, } from '../../../../src/core/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; +import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; import { createDefaultSpace } from './lib/create_default_space'; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index f8ed58fa575518..95bda96d894615 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -8,7 +8,7 @@ import { map, take } from 'rxjs/operators'; import { Observable, Subscription } from 'rxjs'; import { Legacy } from 'kibana'; import { Logger, KibanaRequest, CoreSetup } from '../../../../../src/core/server'; -import { PluginSetupContract as SecurityPluginSetup } from '../../../security/server'; +import { SecurityPluginSetup } from '../../../security/server'; import { LegacyAPI } from '../plugin'; import { SpacesClient } from '../lib/spaces_client'; import { ConfigType } from '../config'; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts index d7bee93f5c94b9..7194c642e7015e 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/aad/index.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import Hapi from 'hapi'; import { Legacy } from 'kibana'; import KbnServer from '../../../../../../../src/legacy/server/kbn_server'; -import { PluginStartContract } from '../../../../../../plugins/encrypted_saved_objects/server'; +import { EncryptedSavedObjectsPluginStart } from '../../../../../../plugins/encrypted_saved_objects/server'; interface CheckAADRequest extends Hapi.Request { payload: { @@ -25,7 +25,8 @@ export default function(kibana: any) { name: 'aad-fixtures', init(server: Legacy.Server) { const newPlatform = ((server as unknown) as KbnServer).newPlatform; - const esoPlugin = newPlatform.start.plugins.encryptedSavedObjects as PluginStartContract; + const esoPlugin = newPlatform.start.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; server.route({ method: 'POST', diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts index a194e477da7559..e61b8f24a1f69b 100644 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts +++ b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts @@ -8,8 +8,8 @@ import { Request } from 'hapi'; import { boomify, badRequest } from 'boom'; import { Legacy } from 'kibana'; import { - PluginSetupContract, - PluginStartContract, + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, } from '../../../../plugins/encrypted_saved_objects/server'; const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; @@ -26,7 +26,7 @@ export default function esoPlugin(kibana: any) { path: '/api/saved_objects/get-decrypted-as-internal-user/{id}', async handler(request: Request) { const encryptedSavedObjectsStart = server.newPlatform.start.plugins - .encryptedSavedObjects as PluginStartContract; + .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request); try { return await encryptedSavedObjectsStart.getDecryptedAsInternalUser( @@ -44,7 +44,8 @@ export default function esoPlugin(kibana: any) { }, }); - (server.newPlatform.setup.plugins.encryptedSavedObjects as PluginSetupContract).registerType({ + (server.newPlatform.setup.plugins + .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup).registerType({ type: SAVED_OBJECT_WITH_SECRET_TYPE, attributesToEncrypt: new Set(['privateProperty']), attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']), From f2713659f63fda25881e1c99087dc084cf4c85a1 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Mon, 3 Feb 2020 09:01:34 -0500 Subject: [PATCH 38/62] filtering (making it work with expressions) (#55351) * adding meta information to KibanaDatatable * updating filtering functions to use new information * moving filter creation to APPLY_FILTER_ACTION * adding SELECT_RANGE_ACTION and TRIGGER * making _meta optional * inlining legacy code for inspector * fixing jest tests * keeping apply_filter_action and adding value_click_action and trigger * utilities for serializing/unserializing aggConfigs * renaming prop to indexPatternId * cleanup * updating interpreter functional baselines * trying to fix tests * Fix legend tests * reverting update to multi metric screenshot * updating based on review * updating tests Co-authored-by: Nick Partridge Co-authored-by: Elastic Machine --- .../public/actions}/filters/brush_event.js | 14 +- .../actions}/filters/brush_event.test.js | 78 +++++++---- .../filters/brush_event.test.mocks.ts | 2 +- .../filters/create_filters_from_event.js} | 51 ++++--- .../public/actions/select_range_action.ts | 91 +++++++++++++ .../data/public/actions/value_click_action.ts | 126 ++++++++++++++++++ src/legacy/core_plugins/data/public/legacy.ts | 6 +- src/legacy/core_plugins/data/public/plugin.ts | 28 +++- .../build_tabular_inspector_data.ts | 4 +- .../search/expressions/create_filter.js | 66 +++++++++ .../data/public/search/expressions/esaggs.ts | 2 + .../data/public/search/expressions/utils.ts | 51 +++++++ .../core_plugins/data/public/search/index.ts | 1 + .../vislib/components/legend/legend.test.tsx | 58 ++++---- .../vislib/components/legend/legend.tsx | 60 +++++---- .../public/embeddable/visualize_embeddable.ts | 36 ++--- .../public/np_ready/public/filters/index.ts | 21 --- .../public/np_ready/public/index.ts | 3 - .../np_ready/public/types/base_vis_type.js | 10 -- src/legacy/ui/public/agg_types/agg_config.ts | 2 +- src/legacy/ui/public/agg_types/agg_types.ts | 92 +++++++++++++ src/legacy/ui/public/agg_types/index.ts | 80 +---------- .../ui/public/agg_types/param_types/field.ts | 4 +- src/legacy/ui/public/agg_types/utils.ts | 2 +- .../public/vis/lib/least_common_interval.ts | 2 +- src/plugins/embeddable/public/bootstrap.ts | 16 +++ src/plugins/embeddable/public/index.ts | 2 + .../embeddable/public/lib/triggers/index.ts | 2 + .../expression_types/kibana_datatable.ts | 7 + .../screenshots/baseline/combined_test.png | Bin 22801 -> 16994 bytes .../baseline/final_screenshot_test.png | Bin 22852 -> 17033 bytes .../screenshots/baseline/metric_all_data.png | Bin 32900 -> 24240 bytes .../baseline/metric_invalid_data.png | Bin 1920 -> 1806 bytes .../baseline/metric_percentage_mode.png | Bin 32587 -> 23705 bytes .../baseline/metric_single_metric_data.png | Bin 29643 -> 22004 bytes .../screenshots/baseline/partial_test_1.png | Bin 15140 -> 11228 bytes .../screenshots/baseline/partial_test_2.png | Bin 22801 -> 16994 bytes .../screenshots/baseline/partial_test_3.png | Bin 7049 -> 7054 bytes .../baseline/tagcloud_all_data.png | Bin 15701 -> 11827 bytes .../baseline/tagcloud_fontsize.png | Bin 13808 -> 10314 bytes .../baseline/tagcloud_invalid_data.png | Bin 1920 -> 1806 bytes .../baseline/tagcloud_metric_data.png | Bin 9163 -> 6712 bytes .../screenshots/baseline/tagcloud_options.png | Bin 18998 -> 15100 bytes .../snapshots/baseline/combined_test2.json | 2 +- .../snapshots/baseline/combined_test3.json | 2 +- .../snapshots/baseline/final_output_test.json | 2 +- .../snapshots/baseline/metric_all_data.json | 2 +- .../baseline/metric_multi_metric_data.json | 2 +- .../baseline/metric_percentage_mode.json | 2 +- .../baseline/metric_single_metric_data.json | 2 +- .../snapshots/baseline/partial_test_1.json | 2 +- .../snapshots/baseline/partial_test_2.json | 2 +- .../snapshots/baseline/partial_test_3.json | 2 +- .../snapshots/baseline/step_output_test2.json | 2 +- .../snapshots/baseline/step_output_test3.json | 2 +- .../snapshots/baseline/tagcloud_all_data.json | 2 +- .../snapshots/baseline/tagcloud_fontsize.json | 2 +- .../baseline/tagcloud_metric_data.json | 2 +- .../snapshots/baseline/tagcloud_options.json | 2 +- .../snapshots/session/combined_test2.json | 2 +- .../snapshots/session/combined_test3.json | 2 +- .../snapshots/session/final_output_test.json | 2 +- .../snapshots/session/metric_all_data.json | 2 +- .../session/metric_multi_metric_data.json | 2 +- .../session/metric_percentage_mode.json | 2 +- .../session/metric_single_metric_data.json | 2 +- .../snapshots/session/partial_test_1.json | 2 +- .../snapshots/session/partial_test_2.json | 2 +- .../snapshots/session/partial_test_3.json | 2 +- .../snapshots/session/step_output_test2.json | 2 +- .../snapshots/session/step_output_test3.json | 2 +- .../snapshots/session/tagcloud_all_data.json | 2 +- .../snapshots/session/tagcloud_fontsize.json | 2 +- .../session/tagcloud_metric_data.json | 2 +- .../snapshots/session/tagcloud_options.json | 2 +- 75 files changed, 700 insertions(+), 281 deletions(-) rename src/legacy/core_plugins/{visualizations/public/np_ready/public => data/public/actions}/filters/brush_event.js (81%) rename src/legacy/core_plugins/{visualizations/public/np_ready/public => data/public/actions}/filters/brush_event.test.js (71%) rename src/legacy/core_plugins/{visualizations/public/np_ready/public => data/public/actions}/filters/brush_event.test.mocks.ts (92%) rename src/legacy/core_plugins/{visualizations/public/np_ready/public/filters/vis_filters.js => data/public/actions/filters/create_filters_from_event.js} (72%) create mode 100644 src/legacy/core_plugins/data/public/actions/select_range_action.ts create mode 100644 src/legacy/core_plugins/data/public/actions/value_click_action.ts create mode 100644 src/legacy/core_plugins/data/public/search/expressions/create_filter.js create mode 100644 src/legacy/core_plugins/data/public/search/expressions/utils.ts delete mode 100644 src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts create mode 100644 src/legacy/ui/public/agg_types/agg_types.ts diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js similarity index 81% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.js index e0854205b132ea..67711bd4599a2d 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.js @@ -19,9 +19,10 @@ import _ from 'lodash'; import moment from 'moment'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; -export function onBrushEvent(event) { +export async function onBrushEvent(event, getIndexPatterns) { const isNumber = event.data.ordered; const isDate = isNumber && event.data.ordered.date; @@ -29,9 +30,12 @@ export function onBrushEvent(event) { if (!xRaw) return []; const column = xRaw.table.columns[xRaw.column]; if (!column) return []; - const aggConfig = event.aggConfigs[xRaw.column]; - if (!aggConfig) return []; - const indexPattern = aggConfig.getIndexPattern(); + if (!column.meta) return []; + const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); + const aggConfig = deserializeAggConfig({ + ...column.meta, + indexPattern, + }); const field = aggConfig.params.field; if (!field) return []; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js similarity index 71% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js index 215d440edd9d02..a6fe58503cd021 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.js +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.js @@ -20,21 +20,36 @@ import _ from 'lodash'; import moment from 'moment'; import expect from '@kbn/expect'; + +jest.mock('../../../../../ui/public/agg_types/agg_configs', () => ({ + AggConfigs: function AggConfigs() { + return { + createAggConfig: ({ params }) => ({ + params, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }), + }; + }, +})); + import { onBrushEvent } from './brush_event'; describe('brushEvent', () => { const DAY_IN_MS = 24 * 60 * 60 * 1000; const JAN_01_2014 = 1388559600000; + const aggConfigs = [ + { + params: {}, + getIndexPattern: () => ({ + timeFieldName: 'time', + }), + }, + ]; + const baseEvent = { - aggConfigs: [ - { - params: {}, - getIndexPattern: () => ({ - timeFieldName: 'time', - }), - }, - ], data: { fieldFormatter: _.constant({}), series: [ @@ -47,6 +62,11 @@ describe('brushEvent', () => { columns: [ { id: '1', + meta: { + type: 'histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: aggConfigs[0].params, + }, }, ], }, @@ -69,9 +89,11 @@ describe('brushEvent', () => { expect(onBrushEvent).to.be.a(Function); }); - test('ignores event when data.xAxisField not provided', () => { + test('ignores event when data.xAxisField not provided', async () => { const event = _.cloneDeep(baseEvent); - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => baseEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); @@ -84,22 +106,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('by ignoring the event when range spans zero time', () => { + test('by ignoring the event when range spans zero time', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by updating the timefilter', () => { + test('by updating the timefilter', async () => { const event = _.cloneDeep(dateEvent); event.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: async () => dateEvent.data.indexPattern, + })); expect(filters[0].range.time.gte).to.be(new Date(JAN_01_2014).toISOString()); // Set to a baseline timezone for comparison. expect(filters[0].range.time.lt).to.be(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); @@ -114,17 +140,19 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = dateField; dateEvent = _.cloneDeep(baseEvent); - dateEvent.aggConfigs[0].params.field = dateField; dateEvent.data.ordered = { date: true }; }); - test('creates a new range filter', () => { + test('creates a new range filter', async () => { const event = _.cloneDeep(dateEvent); const rangeBegin = JAN_01_2014; const rangeEnd = rangeBegin + DAY_IN_MS; event.range = [rangeBegin, rangeEnd]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => dateEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.anotherTimeField.gte).to.equal(moment(rangeBegin).toISOString()); expect(filters[0].range.anotherTimeField.lt).to.equal(moment(rangeEnd).toISOString()); @@ -142,22 +170,26 @@ describe('brushEvent', () => { }; beforeEach(() => { + aggConfigs[0].params.field = numberField; numberEvent = _.cloneDeep(baseEvent); - numberEvent.aggConfigs[0].params.field = numberField; numberEvent.data.ordered = { date: false }; }); - test('by ignoring the event when range does not span at least 2 values', () => { + test('by ignoring the event when range does not span at least 2 values', async () => { const event = _.cloneDeep(numberEvent); event.range = [1]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(0); }); - test('by creating a new filter', () => { + test('by creating a new filter', async () => { const event = _.cloneDeep(numberEvent); event.range = [1, 2, 3, 4]; - const filters = onBrushEvent(event); + const filters = await onBrushEvent(event, () => ({ + get: () => numberEvent.data.indexPattern, + })); expect(filters.length).to.equal(1); expect(filters[0].range.numberField.gte).to.equal(1); expect(filters[0].range.numberField.lt).to.equal(4); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts similarity index 92% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts rename to src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts index f0de2f88dcb82c..2cecfd0fe8b767 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/brush_event.test.mocks.ts +++ b/src/legacy/core_plugins/data/public/actions/filters/brush_event.test.mocks.ts @@ -17,7 +17,7 @@ * under the License. */ -import { chromeServiceMock } from '../../../../../../../core/public/mocks'; +import { chromeServiceMock } from '../../../../../../core/public/mocks'; jest.doMock('ui/new_platform', () => ({ npStart: { diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js similarity index 72% rename from src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js rename to src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js index 303dec690e62bb..1037c718d0003f 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/vis_filters.js +++ b/src/legacy/core_plugins/data/public/actions/filters/create_filters_from_event.js @@ -17,8 +17,10 @@ * under the License. */ -import { onBrushEvent } from './brush_event'; -import { esFilters } from '../../../../../../../plugins/data/public'; +import { esFilters } from '../../../../../../plugins/data/public'; +import { deserializeAggConfig } from '../../search/expressions/utils'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../../plugins/data/public/services'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -63,11 +65,16 @@ const getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { * @param {string} cellValue - value of the current cell * @return {array|string} - filter or list of filters to provide to queryFilter.addFilters() */ -const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { +const createFilter = async (table, columnIndex, rowIndex) => { + if (!table || !table.columns || !table.columns[columnIndex]) return; const column = table.columns[columnIndex]; - const aggConfig = aggConfigs[columnIndex]; + const aggConfig = deserializeAggConfig({ + type: column.meta.type, + aggConfigParams: column.meta.aggConfigParams, + indexPattern: await getIndexPatterns().get(column.meta.indexPatternId), + }); let filter = []; - const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : null; if (value === null || value === undefined || !aggConfig.isFilterable()) { return; } @@ -85,26 +92,28 @@ const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { return filter; }; -const createFiltersFromEvent = event => { +const createFiltersFromEvent = async event => { const filters = []; const dataPoints = event.data || [event]; - dataPoints - .filter(point => point) - .forEach(val => { - const { table, column, row, value } = val; - const filter = createFilter(event.aggConfigs, table, column, row, value); - if (filter) { - filter.forEach(f => { - if (event.negate) { - f = esFilters.toggleFilterNegated(f); - } - filters.push(f); - }); - } - }); + await Promise.all( + dataPoints + .filter(point => point) + .map(async val => { + const { table, column, row } = val; + const filter = await createFilter(table, column, row); + if (filter) { + filter.forEach(f => { + if (event.negate) { + f = esFilters.toggleFilterNegated(f); + } + filters.push(f); + }); + } + }) + ); return filters; }; -export { createFilter, createFiltersFromEvent, onBrushEvent }; +export { createFilter, createFiltersFromEvent }; diff --git a/src/legacy/core_plugins/data/public/actions/select_range_action.ts b/src/legacy/core_plugins/data/public/actions/select_range_action.ts new file mode 100644 index 00000000000000..4ea5c78a9fd2b4 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/select_range_action.ts @@ -0,0 +1,91 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// @ts-ignore +import { onBrushEvent } from './filters/brush_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIndexPatterns } from '../../../../../plugins/data/public/services'; + +export const SELECT_RANGE_ACTION = 'SELECT_RANGE_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await onBrushEvent(context.data, getIndexPatterns)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function selectRangeAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: SELECT_RANGE_ACTION, + id: SELECT_RANGE_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await onBrushEvent(data, getIndexPatterns)) || []; + + const selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/actions/value_click_action.ts b/src/legacy/core_plugins/data/public/actions/value_click_action.ts new file mode 100644 index 00000000000000..2f622eb1eb6695 --- /dev/null +++ b/src/legacy/core_plugins/data/public/actions/value_click_action.ts @@ -0,0 +1,126 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; +import { + IAction, + createAction, + IncompatibleActionError, +} from '../../../../../plugins/ui_actions/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getOverlays, getIndexPatterns } from '../../../../../plugins/data/public/services'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { applyFiltersPopover } from '../../../../../plugins/data/public/ui/apply_filters'; +// @ts-ignore +import { createFiltersFromEvent } from './filters/create_filters_from_event'; +import { + esFilters, + FilterManager, + TimefilterContract, + changeTimeFilter, + extractTimeFilter, + mapAndFlattenFilters, +} from '../../../../../plugins/data/public'; + +export const VALUE_CLICK_ACTION = 'VALUE_CLICK_ACTION'; + +interface ActionContext { + data: any; + timeFieldName: string; +} + +async function isCompatible(context: ActionContext) { + try { + const filters: esFilters.Filter[] = (await createFiltersFromEvent(context.data)) || []; + return filters.length > 0; + } catch { + return false; + } +} + +export function valueClickAction( + filterManager: FilterManager, + timeFilter: TimefilterContract +): IAction { + return createAction({ + type: VALUE_CLICK_ACTION, + id: VALUE_CLICK_ACTION, + getDisplayName: () => { + return i18n.translate('data.filter.applyFilterActionTitle', { + defaultMessage: 'Apply filter to current view', + }); + }, + isCompatible, + execute: async ({ timeFieldName, data }: ActionContext) => { + if (!(await isCompatible({ timeFieldName, data }))) { + throw new IncompatibleActionError(); + } + + const filters: esFilters.Filter[] = (await createFiltersFromEvent(data)) || []; + + let selectedFilters: esFilters.Filter[] = mapAndFlattenFilters(filters); + + if (selectedFilters.length > 1) { + const indexPatterns = await Promise.all( + filters.map(filter => { + return getIndexPatterns().get(filter.meta.index!); + }) + ); + + const filterSelectionPromise: Promise = new Promise(resolve => { + const overlay = getOverlays().openModal( + toMountPoint( + applyFiltersPopover( + filters, + indexPatterns, + () => { + overlay.close(); + resolve([]); + }, + (filterSelection: esFilters.Filter[]) => { + overlay.close(); + resolve(filterSelection); + } + ) + ), + { + 'data-test-subj': 'selectFilterOverlay', + } + ); + }); + + selectedFilters = await filterSelectionPromise; + } + + if (timeFieldName) { + const { timeRangeFilter, restOfFilters } = extractTimeFilter( + timeFieldName, + selectedFilters + ); + filterManager.addFilters(restOfFilters); + if (timeRangeFilter) { + changeTimeFilter(timeFilter, timeRangeFilter); + } + } else { + filterManager.addFilters(selectedFilters); + } + }, + }); +} diff --git a/src/legacy/core_plugins/data/public/legacy.ts b/src/legacy/core_plugins/data/public/legacy.ts index a6646ea338c93c..d37c17c2240727 100644 --- a/src/legacy/core_plugins/data/public/legacy.ts +++ b/src/legacy/core_plugins/data/public/legacy.ts @@ -39,8 +39,6 @@ import { plugin } from '.'; const dataPlugin = plugin(); -export const setup = dataPlugin.setup(npSetup.core); +export const setup = dataPlugin.setup(npSetup.core, npSetup.plugins); -export const start = dataPlugin.start(npStart.core, { - data: npStart.plugins.data, -}); +export const start = dataPlugin.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 6bd85ef020f167..da35366cdff318 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -22,6 +22,7 @@ import { DataPublicPluginStart, addSearchStrategy, defaultSearchStrategy, + DataPublicPluginSetup, } from '../../../../plugins/data/public'; import { ExpressionsSetup } from '../../../../plugins/expressions/public'; @@ -32,15 +33,27 @@ import { setInjectedMetadata, setFieldFormats, setSearchService, + setOverlays, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../plugins/data/public/services'; +import { SELECT_RANGE_ACTION, selectRangeAction } from './actions/select_range_action'; +import { VALUE_CLICK_ACTION, valueClickAction } from './actions/value_click_action'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../plugins/embeddable/public/lib/triggers'; +import { IUiActionsSetup, IUiActionsStart } from '../../../../plugins/ui_actions/public'; export interface DataPluginSetupDependencies { + data: DataPublicPluginSetup; expressions: ExpressionsSetup; + uiActions: IUiActionsSetup; } export interface DataPluginStartDependencies { data: DataPublicPluginStart; + uiActions: IUiActionsStart; } /** @@ -64,19 +77,30 @@ export interface DataStart {} // eslint-disable-line @typescript-eslint/no-empty export class DataPlugin implements Plugin { - public setup(core: CoreSetup) { + public setup(core: CoreSetup, { data, uiActions }: DataPluginSetupDependencies) { setInjectedMetadata(core.injectedMetadata); // This is to be deprecated once we switch to the new search service fully addSearchStrategy(defaultSearchStrategy); + + uiActions.registerAction( + selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) + ); + uiActions.registerAction( + valueClickAction(data.query.filterManager, data.query.timefilter.timefilter) + ); } - public start(core: CoreStart, { data }: DataPluginStartDependencies): DataStart { + public start(core: CoreStart, { data, uiActions }: DataPluginStartDependencies): DataStart { setUiSettings(core.uiSettings); setQueryService(data.query); setIndexPatterns(data.indexPatterns); setFieldFormats(data.fieldFormats); setSearchService(data.search); + setOverlays(core.overlays); + + uiActions.attachAction(SELECT_RANGE_TRIGGER, SELECT_RANGE_ACTION); + uiActions.attachAction(VALUE_CLICK_TRIGGER, VALUE_CLICK_ACTION); return {}; } diff --git a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts index 6e6d2a15fa2ac8..8f7953c408a971 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -19,9 +19,9 @@ import { set } from 'lodash'; // @ts-ignore -import { createFilter } from '../../../../visualizations/public'; import { FormattedData } from '../../../../../../plugins/inspector/public'; - +// @ts-ignore +import { createFilter } from './create_filter'; interface Column { id: string; name: string; diff --git a/src/legacy/core_plugins/data/public/search/expressions/create_filter.js b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js new file mode 100644 index 00000000000000..3f4028a9b5525b --- /dev/null +++ b/src/legacy/core_plugins/data/public/search/expressions/create_filter.js @@ -0,0 +1,66 @@ +/* + * 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 getOtherBucketFilterTerms = (table, columnIndex, rowIndex) => { + if (rowIndex === -1) { + return []; + } + + // get only rows where cell value matches current row for all the fields before columnIndex + const rows = table.rows.filter(row => { + return table.columns.every((column, i) => { + return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex; + }); + }); + const terms = rows.map(row => row[table.columns[columnIndex].id]); + + return [ + ...new Set( + terms.filter(term => { + const notOther = term !== '__other__'; + const notMissing = term !== '__missing__'; + return notOther && notMissing; + }) + ), + ]; +}; + +const createFilter = (aggConfigs, table, columnIndex, rowIndex, cellValue) => { + const column = table.columns[columnIndex]; + const aggConfig = aggConfigs[columnIndex]; + let filter = []; + const value = rowIndex > -1 ? table.rows[rowIndex][column.id] : cellValue; + if (value === null || value === undefined || !aggConfig.isFilterable()) { + return; + } + if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) { + const terms = getOtherBucketFilterTerms(table, columnIndex, rowIndex); + filter = aggConfig.createFilter(value, { terms }); + } else { + filter = aggConfig.createFilter(value); + } + + if (!Array.isArray(filter)) { + filter = [filter]; + } + + return filter; +}; + +export { createFilter }; diff --git a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts index 143283152d1044..b4ea2cd378d61b 100644 --- a/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts +++ b/src/legacy/core_plugins/data/public/search/expressions/esaggs.ts @@ -46,6 +46,7 @@ import { Adapters } from '../../../../../../plugins/inspector/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getQueryService, getIndexPatterns } from '../../../../../../plugins/data/public/services'; import { getRequestInspectorStats, getResponseInspectorStats } from '../..'; +import { serializeAggConfig } from './utils'; export interface RequestHandlerParams { searchSource: ISearchSource; @@ -289,6 +290,7 @@ export const esaggs = (): ExpressionFunction { + return { + type: aggConfig.type.name, + indexPatternId: aggConfig.getIndexPattern().id, + aggConfigParams: aggConfig.toJSON().params, + }; +}; + +interface DeserializeAggConfigParams { + type: string; + aggConfigParams: Record; + indexPattern: IndexPattern; +} + +export const deserializeAggConfig = ({ + type, + aggConfigParams, + indexPattern, +}: DeserializeAggConfigParams) => { + const aggConfigs = new AggConfigs(indexPattern); + const aggConfig = aggConfigs.createAggConfig({ + enabled: true, + type, + params: aggConfigParams, + }); + return aggConfig; +}; diff --git a/src/legacy/core_plugins/data/public/search/index.ts b/src/legacy/core_plugins/data/public/search/index.ts index e1c93ec0e3b1c4..c975d5772e0a8b 100644 --- a/src/legacy/core_plugins/data/public/search/index.ts +++ b/src/legacy/core_plugins/data/public/search/index.ts @@ -18,3 +18,4 @@ */ export { getRequestInspectorStats, getResponseInspectorStats } from './utils'; +export { serializeAggConfig } from './expressions/utils'; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index 6f0a5a3784b070..e66dff01b6bf21 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -34,8 +34,8 @@ jest.mock('@elastic/eui', () => ({ jest.mock('../../../legacy_imports', () => ({ getTableAggs: jest.fn(), })); -jest.mock('../../../../../visualizations/public', () => ({ - createFiltersFromEvent: jest.fn().mockReturnValue(['yes']), +jest.mock('../../../../../data/public/actions/filters/create_filters_from_event', () => ({ + createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), })); const vis = { @@ -95,8 +95,8 @@ const uiState = { setSilent: jest.fn(), }; -const getWrapper = (props?: Partial) => - mount( +const getWrapper = async (props?: Partial) => { + const wrapper = mount( ) => ); + await (wrapper.find(VisLegend).instance() as VisLegend).refresh(); + wrapper.update(); + return wrapper; +}; + const getLegendItems = (wrapper: ReactWrapper) => wrapper.find('.visLegend__button'); describe('VisLegend Component', () => { @@ -120,9 +125,9 @@ describe('VisLegend Component', () => { }); describe('Legend open', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -131,9 +136,9 @@ describe('VisLegend Component', () => { }); describe('Legend closed', () => { - beforeEach(() => { + beforeEach(async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); }); it('should match the snapshot', () => { @@ -142,25 +147,26 @@ describe('VisLegend Component', () => { }); describe('Highlighting', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); - it('should call highlight handler when legend item is focused', () => { + it('should call highlight handler when legend item is focused', async () => { const first = getLegendItems(wrapper).first(); + first.simulate('focus'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call highlight handler when legend item is hovered', () => { + it('should call highlight handler when legend item is hovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); expect(vislibVis.handler.highlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is blurred', () => { + it('should call unHighlight handler when legend item is blurred', async () => { let first = getLegendItems(wrapper).first(); first.simulate('focus'); first = getLegendItems(wrapper).first(); @@ -169,7 +175,7 @@ describe('VisLegend Component', () => { expect(vislibVis.handler.unHighlight).toHaveBeenCalledTimes(1); }); - it('should call unHighlight handler when legend item is unhovered', () => { + it('should call unHighlight handler when legend item is unhovered', async () => { const first = getLegendItems(wrapper).first(); first.simulate('mouseEnter'); @@ -187,8 +193,8 @@ describe('VisLegend Component', () => { }, }; - expect(() => { - wrapper = getWrapper({ vis: newVis }); + expect(async () => { + wrapper = await getWrapper({ vis: newVis }); const first = getLegendItems(wrapper).first(); first.simulate('focus'); first.simulate('blur'); @@ -197,8 +203,8 @@ describe('VisLegend Component', () => { }); describe('Filtering', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should filter out when clicked', () => { @@ -223,8 +229,8 @@ describe('VisLegend Component', () => { }); describe('Toggles details', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('should show details when clicked', () => { @@ -236,8 +242,8 @@ describe('VisLegend Component', () => { }); describe('setColor', () => { - beforeEach(() => { - wrapper = getWrapper(); + beforeEach(async () => { + wrapper = await getWrapper(); }); it('sets the color in the UI state', () => { @@ -255,18 +261,18 @@ describe('VisLegend Component', () => { }); describe('toggleLegend function', () => { - it('click should show legend once toggled from hidden', () => { + it('click should show legend once toggled from hidden', async () => { mockState.set('vis.legendOpen', false); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); expect(wrapper.exists('.visLegend__list')).toBe(true); }); - it('click should hide legend once toggled from shown', () => { + it('click should hide legend once toggled from shown', async () => { mockState.set('vis.legendOpen', true); - wrapper = getWrapper(); + wrapper = await getWrapper(); const toggleButton = wrapper.find('.visLegend__toggle').first(); toggleButton.simulate('click'); diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index 0eec557dd334ee..a170af33583df0 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -24,7 +24,8 @@ import { i18n } from '@kbn/i18n'; import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui'; // @ts-ignore -import { createFiltersFromEvent } from '../../../../../visualizations/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createFiltersFromEvent } from '../../../../../data/public/actions/filters/create_filters_from_event'; import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models'; import { VisLegendItem } from './legend_item'; import { getPieNames } from './pie_utils'; @@ -94,11 +95,11 @@ export class VisLegend extends PureComponent { this.props.vis.API.events.filter({ data, negate }); }; - canFilter = (item: LegendItem): boolean => { + canFilter = async (item: LegendItem): Promise => { if (CUSTOM_LEGEND_VIS_TYPES.includes(this.props.vislibVis.visConfigArgs.type)) { return false; } - const filters = createFiltersFromEvent({ aggConfigs: this.state.tableAggs, data: item.values }); + const filters = await createFiltersFromEvent({ data: item.values }); return Boolean(filters.length); }; @@ -123,16 +124,39 @@ export class VisLegend extends PureComponent { }; // Most of these functions were moved directly from the old Legend class. Not a fan of this. - getLabels = (data: any, type: string) => { - if (!data) return []; - data = data.columns || data.rows || [data]; + setLabels = (data: any, type: string): Promise => + new Promise(async resolve => { + let labels = []; + if (CUSTOM_LEGEND_VIS_TYPES.includes(type)) { + const legendLabels = this.props.vislibVis.getLegendLabels(); + if (legendLabels) { + labels = map(legendLabels, label => { + return { label }; + }); + } + } else { + if (!data) return []; + data = data.columns || data.rows || [data]; - if (type === 'pie') return getPieNames(data); + labels = type === 'pie' ? getPieNames(data) : this.getSeriesLabels(data); + } - return this.getSeriesLabels(data); - }; + const labelsConfig = await Promise.all( + labels.map(async label => ({ + ...label, + canFilter: await this.canFilter(label), + })) + ); + + this.setState( + { + labels: labelsConfig, + }, + resolve + ); + }); - refresh = () => { + refresh = async () => { const vislibVis = this.props.vislibVis; if (!vislibVis || !vislibVis.visConfig) { this.setState({ @@ -154,24 +178,12 @@ export class VisLegend extends PureComponent { this.setState({ open: this.props.vis.params.addLegend }); } - if (CUSTOM_LEGEND_VIS_TYPES.includes(vislibVis.visConfigArgs.type)) { - const legendLabels = this.props.vislibVis.getLegendLabels(); - if (legendLabels) { - this.setState({ - labels: map(legendLabels, label => { - return { label }; - }), - }); - } - } else { - this.setState({ labels: this.getLabels(this.props.visData, vislibVis.visConfigArgs.type) }); - } - if (vislibVis.visConfig) { this.getColor = this.props.vislibVis.visConfig.data.getColorFunc(); } this.setState({ tableAggs: getTableAggs(this.props.vis) }); + await this.setLabels(this.props.visData, vislibVis.visConfigArgs.type); }; highlight = (event: BaseSyntheticEvent) => { @@ -219,7 +231,7 @@ export class VisLegend extends PureComponent { key={item.label} anchorPosition={anchorPosition} selected={this.state.selectedLabel === item.label} - canFilter={this.canFilter(item)} + canFilter={item.canFilter} onFilter={this.filter} onSelect={this.toggleDetails} legendId={this.legendId} diff --git a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts index 2af468ff77de6e..d3badcc6bdc3fc 100644 --- a/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -17,13 +17,12 @@ * under the License. */ -import _, { forEach } from 'lodash'; +import _ from 'lodash'; import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; import { SavedObject } from 'ui/saved_objects/types'; -import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { AppState } from 'ui/state_management/app_state'; import { npStart } from 'ui/new_platform'; import { IExpressionLoaderParams } from 'src/plugins/expressions/public'; @@ -34,7 +33,6 @@ import { Query, onlyDisabledFiltersChanged, esFilters, - mapAndFlattenFilters, ISearchSource, } from '../../../../../plugins/data/public'; import { @@ -42,7 +40,8 @@ import { EmbeddableOutput, Embeddable, Container, - APPLY_FILTER_TRIGGER, + VALUE_CLICK_TRIGGER, + SELECT_RANGE_TRIGGER, } from '../../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../../plugins/kibana_utils/public'; import { SavedSearch } from '../../../kibana/public/discover/np_ready/types'; @@ -105,7 +104,6 @@ export class VisualizeEmbeddable extends Embeddable { - if (event.disabled || !eventName) { - return; - } else { - this.actions[eventName] = event.defaultAction; - } - }); - // This is a hack to give maps visualizations access to data in the // globalState, since they can no longer access it via searchSource. // TODO: Remove this as a part of elastic/kibana#30593 @@ -301,18 +290,13 @@ export class VisualizeEmbeddable extends Embeddable { - if (this.actions[event.name]) { - event.data.aggConfigs = getTableAggs(this.vis); - const filters: esFilters.Filter[] = this.actions[event.name](event.data) || []; - const mappedFilters = mapAndFlattenFilters(filters); - const timeFieldName = this.vis.indexPattern.timeFieldName; - - npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, { - embeddable: this, - filters: mappedFilters, - timeFieldName, - }); - } + const eventName = event.name === 'brush' ? SELECT_RANGE_TRIGGER : VALUE_CLICK_TRIGGER; + + npStart.plugins.uiActions.executeTriggerActions(eventName, { + embeddable: this, + timeFieldName: this.vis.indexPattern.timeFieldName, + data: event.data, + }); }) ); diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts deleted file mode 100644 index 4558621dc6615a..00000000000000 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/filters/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// @ts-ignore -export * from './vis_filters'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts index 4dffcb8ce995e3..3c4a1c1449d475 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/index.ts @@ -44,7 +44,6 @@ export function plugin(initializerContext: PluginInitializerContext) { /** @public static code */ export { Vis, VisParams, VisState } from './vis'; -export * from './filters'; export { TypesService } from './types/types_service'; export { Status } from './legacy/update_status'; @@ -53,6 +52,4 @@ export { buildPipeline, buildVislibDimensions, SchemaConfig } from './legacy/bui // @ts-ignore export { updateOldState } from './legacy/vis_update_state'; export { calculateObjectHash } from './legacy/calculate_object_hash'; -// @ts-ignore -export { createFiltersFromEvent } from './filters/vis_filters'; export { createSavedVisLoader } from '../../saved_visualizations/saved_visualizations'; diff --git a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js index f62b3a0b393aca..351acc48e26766 100644 --- a/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js +++ b/src/legacy/core_plugins/visualizations/public/np_ready/public/types/base_vis_type.js @@ -19,7 +19,6 @@ import _ from 'lodash'; -import { createFiltersFromEvent, onBrushEvent } from '../filters'; import { DefaultEditorController } from '../../../../../vis_default_editor/public'; export class BaseVisType { @@ -60,15 +59,6 @@ export class BaseVisType { showIndexSelection: true, hierarchicalData: false, // we should get rid of this i guess ? }, - events: { - filterBucket: { - defaultAction: createFiltersFromEvent, - }, - brush: { - defaultAction: onBrushEvent, - disabled: true, - }, - }, stage: 'production', feedbackMessage: '', hidden: false, diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index 3f88c540be1641..17a8b14b57d020 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -63,7 +63,7 @@ const unknownSchema: Schema = { const getTypeFromRegistry = (type: string): AggType => { // We need to inline require here, since we're having a cyclic dependency // from somewhere inside agg_types back to AggConfig. - const aggTypes = require('../agg_types').aggTypes; + const aggTypes = require('./agg_types').aggTypes; const registeredType = aggTypes.metrics.find((agg: AggType) => agg.name === type) || aggTypes.buckets.find((agg: AggType) => agg.name === type); diff --git a/src/legacy/ui/public/agg_types/agg_types.ts b/src/legacy/ui/public/agg_types/agg_types.ts new file mode 100644 index 00000000000000..1b05f5926ebfc9 --- /dev/null +++ b/src/legacy/ui/public/agg_types/agg_types.ts @@ -0,0 +1,92 @@ +/* + * 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 { countMetricAgg } from './metrics/count'; +import { avgMetricAgg } from './metrics/avg'; +import { sumMetricAgg } from './metrics/sum'; +import { medianMetricAgg } from './metrics/median'; +import { minMetricAgg } from './metrics/min'; +import { maxMetricAgg } from './metrics/max'; +import { topHitMetricAgg } from './metrics/top_hit'; +import { stdDeviationMetricAgg } from './metrics/std_deviation'; +import { cardinalityMetricAgg } from './metrics/cardinality'; +import { percentilesMetricAgg } from './metrics/percentiles'; +import { geoBoundsMetricAgg } from './metrics/geo_bounds'; +import { geoCentroidMetricAgg } from './metrics/geo_centroid'; +import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; +import { derivativeMetricAgg } from './metrics/derivative'; +import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; +import { movingAvgMetricAgg } from './metrics/moving_avg'; +import { serialDiffMetricAgg } from './metrics/serial_diff'; +import { dateHistogramBucketAgg } from './buckets/date_histogram'; +import { histogramBucketAgg } from './buckets/histogram'; +import { rangeBucketAgg } from './buckets/range'; +import { dateRangeBucketAgg } from './buckets/date_range'; +import { ipRangeBucketAgg } from './buckets/ip_range'; +import { termsBucketAgg } from './buckets/terms'; +import { filterBucketAgg } from './buckets/filter'; +import { filtersBucketAgg } from './buckets/filters'; +import { significantTermsBucketAgg } from './buckets/significant_terms'; +import { geoHashBucketAgg } from './buckets/geo_hash'; +import { geoTileBucketAgg } from './buckets/geo_tile'; +import { bucketSumMetricAgg } from './metrics/bucket_sum'; +import { bucketAvgMetricAgg } from './metrics/bucket_avg'; +import { bucketMinMetricAgg } from './metrics/bucket_min'; +import { bucketMaxMetricAgg } from './metrics/bucket_max'; + +export { AggType } from './agg_type'; + +export const aggTypes = { + metrics: [ + countMetricAgg, + avgMetricAgg, + sumMetricAgg, + medianMetricAgg, + minMetricAgg, + maxMetricAgg, + stdDeviationMetricAgg, + cardinalityMetricAgg, + percentilesMetricAgg, + percentileRanksMetricAgg, + topHitMetricAgg, + derivativeMetricAgg, + cumulativeSumMetricAgg, + movingAvgMetricAgg, + serialDiffMetricAgg, + bucketAvgMetricAgg, + bucketSumMetricAgg, + bucketMinMetricAgg, + bucketMaxMetricAgg, + geoBoundsMetricAgg, + geoCentroidMetricAgg, + ], + buckets: [ + dateHistogramBucketAgg, + histogramBucketAgg, + rangeBucketAgg, + dateRangeBucketAgg, + ipRangeBucketAgg, + termsBucketAgg, + filterBucketAgg, + filtersBucketAgg, + significantTermsBucketAgg, + geoHashBucketAgg, + geoTileBucketAgg, + ], +}; diff --git a/src/legacy/ui/public/agg_types/index.ts b/src/legacy/ui/public/agg_types/index.ts index ca7c2f82023c94..cf2733b9a9f36a 100644 --- a/src/legacy/ui/public/agg_types/index.ts +++ b/src/legacy/ui/public/agg_types/index.ts @@ -17,80 +17,7 @@ * under the License. */ -import { countMetricAgg } from './metrics/count'; -import { avgMetricAgg } from './metrics/avg'; -import { sumMetricAgg } from './metrics/sum'; -import { medianMetricAgg } from './metrics/median'; -import { minMetricAgg } from './metrics/min'; -import { maxMetricAgg } from './metrics/max'; -import { topHitMetricAgg } from './metrics/top_hit'; -import { stdDeviationMetricAgg } from './metrics/std_deviation'; -import { cardinalityMetricAgg } from './metrics/cardinality'; -import { percentilesMetricAgg } from './metrics/percentiles'; -import { geoBoundsMetricAgg } from './metrics/geo_bounds'; -import { geoCentroidMetricAgg } from './metrics/geo_centroid'; -import { percentileRanksMetricAgg } from './metrics/percentile_ranks'; -import { derivativeMetricAgg } from './metrics/derivative'; -import { cumulativeSumMetricAgg } from './metrics/cumulative_sum'; -import { movingAvgMetricAgg } from './metrics/moving_avg'; -import { serialDiffMetricAgg } from './metrics/serial_diff'; -import { dateHistogramBucketAgg, setBounds } from './buckets/date_histogram'; -import { histogramBucketAgg } from './buckets/histogram'; -import { rangeBucketAgg } from './buckets/range'; -import { dateRangeBucketAgg } from './buckets/date_range'; -import { ipRangeBucketAgg } from './buckets/ip_range'; -import { termsBucketAgg, termsAggFilter } from './buckets/terms'; -import { filterBucketAgg } from './buckets/filter'; -import { filtersBucketAgg } from './buckets/filters'; -import { significantTermsBucketAgg } from './buckets/significant_terms'; -import { geoHashBucketAgg } from './buckets/geo_hash'; -import { geoTileBucketAgg } from './buckets/geo_tile'; -import { bucketSumMetricAgg } from './metrics/bucket_sum'; -import { bucketAvgMetricAgg } from './metrics/bucket_avg'; -import { bucketMinMetricAgg } from './metrics/bucket_min'; -import { bucketMaxMetricAgg } from './metrics/bucket_max'; - -export { AggType } from './agg_type'; - -export const aggTypes = { - metrics: [ - countMetricAgg, - avgMetricAgg, - sumMetricAgg, - medianMetricAgg, - minMetricAgg, - maxMetricAgg, - stdDeviationMetricAgg, - cardinalityMetricAgg, - percentilesMetricAgg, - percentileRanksMetricAgg, - topHitMetricAgg, - derivativeMetricAgg, - cumulativeSumMetricAgg, - movingAvgMetricAgg, - serialDiffMetricAgg, - bucketAvgMetricAgg, - bucketSumMetricAgg, - bucketMinMetricAgg, - bucketMaxMetricAgg, - geoBoundsMetricAgg, - geoCentroidMetricAgg, - ], - buckets: [ - dateHistogramBucketAgg, - histogramBucketAgg, - rangeBucketAgg, - dateRangeBucketAgg, - ipRangeBucketAgg, - termsBucketAgg, - filterBucketAgg, - filtersBucketAgg, - significantTermsBucketAgg, - geoHashBucketAgg, - geoTileBucketAgg, - ], -}; - +export { aggTypes } from './agg_types'; export { AggParam } from './agg_params'; export { AggConfig } from './agg_config'; export { AggConfigs } from './agg_configs'; @@ -99,5 +26,6 @@ export { FieldParamType } from './param_types'; export { BUCKET_TYPES } from './buckets/bucket_agg_types'; export { METRIC_TYPES } from './metrics/metric_agg_types'; export { ISchemas, Schema, Schemas } from './schemas'; - -export { setBounds, termsAggFilter }; +export { AggType } from './agg_type'; +export { setBounds } from './buckets/date_histogram'; +export { termsAggFilter } from './buckets/terms'; diff --git a/src/legacy/ui/public/agg_types/param_types/field.ts b/src/legacy/ui/public/agg_types/param_types/field.ts index 4ce5bb29f8ff60..d01e059c6c616b 100644 --- a/src/legacy/ui/public/agg_types/param_types/field.ts +++ b/src/legacy/ui/public/agg_types/param_types/field.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { AggConfig } from '../agg_config'; import { SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; import { BaseParamType } from './base'; -import { toastNotifications } from '../../notify'; +import { npStart } from '../../new_platform'; import { propFilter } from '../filter'; import { Field, IFieldList } from '../../../../../plugins/data/public'; import { isNestedField } from '../../../../../plugins/data/public'; @@ -89,7 +89,7 @@ export class FieldParamType extends BaseParamType { (f: any) => f.name === fieldName ); if (!validField) { - toastNotifications.addDanger( + npStart.core.notifications.toasts.addDanger( i18n.translate( 'common.ui.aggTypes.paramTypes.field.invalidSavedFieldParameterErrorMessage', { diff --git a/src/legacy/ui/public/agg_types/utils.ts b/src/legacy/ui/public/agg_types/utils.ts index fd405d49625eda..e382f821b31a90 100644 --- a/src/legacy/ui/public/agg_types/utils.ts +++ b/src/legacy/ui/public/agg_types/utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { isValidEsInterval } from '../../../core_plugins/data/public'; +import { isValidEsInterval } from '../../../core_plugins/data/common/parse_es_interval/is_valid_es_interval'; import { leastCommonInterval } from '../vis/lib/least_common_interval'; /** diff --git a/src/legacy/ui/public/vis/lib/least_common_interval.ts b/src/legacy/ui/public/vis/lib/least_common_interval.ts index 244bc1d0111e3b..72426855f70af5 100644 --- a/src/legacy/ui/public/vis/lib/least_common_interval.ts +++ b/src/legacy/ui/public/vis/lib/least_common_interval.ts @@ -19,7 +19,7 @@ import dateMath from '@elastic/datemath'; import { leastCommonMultiple } from './least_common_multiple'; -import { parseEsInterval } from '../../../../core_plugins/data/public'; +import { parseEsInterval } from '../../../../core_plugins/data/common/parse_es_interval/parse_es_interval'; /** * Finds the lowest common interval between two given ES date histogram intervals diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index 801329a4a79af9..1e0e7dfdb09331 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -23,6 +23,8 @@ import { APPLY_FILTER_TRIGGER, createFilterAction, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, } from './lib'; /** @@ -50,11 +52,25 @@ export const bootstrap = (uiActions: IUiActionsSetup) => { description: 'Actions appear in title bar when an embeddable loads in a panel', actionIds: [], }; + const selectRangeTrigger = { + id: SELECT_RANGE_TRIGGER, + title: 'Select range', + description: 'Applies a range filter', + actionIds: [], + }; + const valueClickTrigger = { + id: VALUE_CLICK_TRIGGER, + title: 'Value clicked', + description: 'Value was clicked', + actionIds: [], + }; const actionApplyFilter = createFilterAction(); uiActions.registerTrigger(triggerContext); uiActions.registerTrigger(triggerFilter); uiActions.registerAction(actionApplyFilter); uiActions.registerTrigger(triggerBadge); + uiActions.registerTrigger(selectRangeTrigger); + uiActions.registerTrigger(valueClickTrigger); // uiActions.attachAction(triggerFilter.id, actionApplyFilter.id); }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 583b21ddfa4338..ec71a1e724c7d8 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -25,6 +25,8 @@ export { APPLY_FILTER_ACTION, APPLY_FILTER_TRIGGER, PANEL_BADGE_TRIGGER, + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, Adapters, AddPanelAction, CONTEXT_MENU_TRIGGER, diff --git a/src/plugins/embeddable/public/lib/triggers/index.ts b/src/plugins/embeddable/public/lib/triggers/index.ts index ffa7f6d0c0f448..72565b3f527adc 100644 --- a/src/plugins/embeddable/public/lib/triggers/index.ts +++ b/src/plugins/embeddable/public/lib/triggers/index.ts @@ -19,4 +19,6 @@ export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; +export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; +export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; export const PANEL_BADGE_TRIGGER = 'PANEL_BADGE_TRIGGER'; diff --git a/src/plugins/expressions/common/expression_types/kibana_datatable.ts b/src/plugins/expressions/common/expression_types/kibana_datatable.ts index c360a2be8c7f7b..38227d2ed6207b 100644 --- a/src/plugins/expressions/common/expression_types/kibana_datatable.ts +++ b/src/plugins/expressions/common/expression_types/kibana_datatable.ts @@ -23,9 +23,16 @@ import { Datatable, PointSeries } from '.'; const name = 'kibana_datatable'; +export interface KibanaDatatableColumnMeta { + type: string; + indexPatternId?: string; + aggConfigParams?: Record; +} + export interface KibanaDatatableColumn { id: string; name: string; + meta?: KibanaDatatableColumnMeta; formatHint?: SerializedFieldFormat; } diff --git a/test/interpreter_functional/screenshots/baseline/combined_test.png b/test/interpreter_functional/screenshots/baseline/combined_test.png index 87f3173d5602472d092da015c8e58f354f0f9bb3..56c055b8b1cff85d7dd891750efdcf6aa198e937 100644 GIT binary patch literal 16994 zcmeIZbySpJ`!8&Q5+d!;jfmvXsdOVPEj37YmkJ^sLk+EhAX3r{DP03dN+T`OL-*P9 zeV*Sr>$l$Xo^}3t*LweWW-VvNnfso5?`vQCx;}9e{z6S2ABO_x#*G{JiV8BCH*VaC zxN+m=(|cIplOp? z`gZfxnW$cg2{R|>11>Hu^m0!J?lp%(51jd{b{Q2XC#P7r<6IPMt>j_T)#^i5wXDUN z$$g_{m)nAZf?2*#h<(t@t1F+mG&;M|&AG^1Cby;gr5eb?&FUg?gpd`(*50>KV&{IAl+sb>LVPteHlNA>9^=9)t$;hejI; zIq$N!wlUUbf0o5rT3S-A$ioMtg4%Rs#a-G@HDvz0ZT8OC_qhjy@Qy2(uGe?R;)cbn0wrGzJ2d#_uybTv*!HwnqhR;WTH)le#5fX zW)fpp(4(`tY44P$&u!~{plMgPKYS0me!iK+rX`2fJVKzH6d@F>nqITN9jC#hd+bR4 zdb;kedPGi_+Rwt{>5vcduLuYTygH+3x#-E%K|j{~s%w#rLb1AMtO-_^vvZ+G=l@0y!U z=V=3zfbJg!Mnkl^7#S8HJBEejmPe9vEvQK7SHr^-U~$@lPIJqRpLX`=ejXRRXu>1z zid}SZxf;afv!{Bdg5rSx9hBtySuAhrSfO3ejTn+vw8;;;j22A8CL$-7*EDXV=y>y1 z8d;od>Jf?$QLr^Q7-H>i-kSD$V-kP`*B%<;xpN5Vc6cscKU^+LtT~*-u2(Ssm07P; zE?&E=tSky57|yfAvUg@z%4^ptqiGoV<^h#xAUt=f%#GknpDyFRmU@{@o6^k8Ox%Z! z*< zUdhPICr}lMdM<5Qv^1B{?mX~~Ongxc3*aFuNQ9cv-a3nrVErU2})*jV}h8szDijg;*^ zn_e`?(>P6YbH@cRmZ_?ZT+)7nYU{vVbIr6bb&~RdC->#kg=|DJ>knC&FZa6i!DVw3 zcbcx>?lg(MyRaHPUxIKu-($1dgRA@G_=r+D^y#vDYzVS48#f2ucA~}Q;o)J*udNjV z=NZ*|MkmI_#XUKLTkUF$!WxPf!1LnFQh!|KovpTr-}p z7gOOcN4hVWVjgK{(FTi;?V2aLKYGc-4SIDBn4dg}Sbj)L8>^1|n~T*v6)o(wEp}_? z0Yuh7AQLPB)Pd)fe0aui6z;AEwb=m4jHzylC11_ro42!-ZP0?Df=u-8GNXpa2hp>@4H z!_Ro@R4gZy6L9`u_wd&TXR^R5$^;U<5+kbv1c3`#Qx(O+>&yATSY3yDx}ejo8cBqN z1x#=IwfzR%fwnCa#%G4Rp*UQJe;UHzOt!r%dJ$n;NW5U9ixNZ~1 zlQ^#rTRqXY9mzA+(=@cVr^3u~fPn$cU>f9h&w8YA@EN?78_!=UCD%nd=?U$a2R@&tUw%&kX*IwkA(3Uq z*!lY&)cUU5)|p4fbANxaB^H$h#Y)Td{PvEHr6RRV5mn3}y}BQRML7i(VeysqVAfK~ zNVChqgo}XvL~D;$z6*=TMY?$@dd1(nKIoya+v@M^665-T%5}Gwr;`SoLr6QPR;gm4 z$WvT0i_)u0-{D-B02(Gv&KMS_%9Wb=>mNmp=MP@RGShN(nV6Vlq0*sAIP7aDnhOl39 zZLK$avESq}RmA5h?BZlZqr@b-l_u1T+iDm)ky8hLF6@8eC*rjsZEyb+R&3au`uz6! z!s=>7U5p|kA_@y@2-BC1Ac1M@;?XckhaTYlp>2P2 z^&FK(jk>t2%)PPeCI8r@=<&d-@y)x4Bu-iG*gJF>@9LtNLu37 zd+vNOz4qcM^qkGJpR2|38{4%DYs^!liBw;Qi*=((v!RZy+M9%<)^rIhF{_FiZKd=B zxyB#1XKG!Dt%=BNnVCV^;Ld4h?|4Y)!UR&8wR<<-f~U|e-}5O>)^l@@b*k<<{yy#v zus(`?V8T%lEB}U60jsC5{?z)1&}zr&aB=XlN_>h5Q<=z}YV*mi7DN^3JBu%?F6V#H z_<1_M8bB#N3T+9tM8&<;VV5wU96p_}7+y|g8h?+wFkvw}rH*Md?cV@E60I)oDC&+R z508myp^1+tNBW=Q!1x{isK5M5nmx?ygUC~*cnC&qJwS!1@|wSyMS|Vdw7S@#9JcRW zp%;YuJMjrXl^9Dp2J{{Mnm{Ow#1DCo`j(-Y?m&j{y(~d|Xo+d_Tj)g1H66@h>N}Ai zCckEoSuIf^A$L$<-zcEU9gUUi+}F7afA62BUMyfE_+5tG+j%~h7%!b2QX)vnY&%9q z655}<{CfZt;>odFdj_g`KX{^f>^;Y~sk6DlVA-2U z2<0@cQ?V8G-epJiCiAl&8655T!?lcz#9)>7Ly0P>yym6lR!Z^5lVXUyo^qWq3qHFc znhLOgCm8*ziz@BsLY6U20QFcim-{_#^P5K3Rn%Ljep`gg!5rAD;4@Q&zZnV;2je;Aa1Bd;>%l1|l1Bk0AC(lwL%$ie`U0 z^e$AusSG|fkR=)m!TOII|cvO5;6%yV-_Y{Z9eTY#fD=DTv8JP1xQw?wT zJxnqOGa^wm0pa6-x$lhMB#d8vMF(&Xm~d{$C1|p|LGPw&UMl6qpNVk+AEQbQXO}R$|h5dvVHbaT(e# z)>|^`$J5r=M=Z~FdRBD$k3vAx+N#K^^^p6t)} zq3e0qKqGgd!v-1X4A_F8%qXs+)XN<9Z8dxMh*^1ZUdOlQR)RVeKjpd>hy@iB6LZ$3 zr~iZ)%JE~R`r@=h6XhQy7G5REhbRAcN;vL^=gotSYfklu`?P9ZQ3R|Fv5Zdj>FlvYMCG(l5s%lwId~Kku~^Z!6^iVBl_tDQ;WBM0(lhaa$e-AEnBS3WlB9vc z1}`33YyW8;+~0%yT=*Xd#RfYO1bl#q`?<@>Fd+exf#ZN<;(jSLNGa8UjW@t~G|r)! zu5RN*#&w$5F5qxj9|x=5jj9%v39r)@~NMDk^x}+uLHj*cPm2a=!-N3MtJs_;?R@ zw6}kUZDA3y8;wXXpxgjL~qgBrEG7#gO4+XYb(n&pI8bW<-8ItIc!Fp5B<)BPKa>dcHYS z`$_Wl8TMYThzpt=<^Ac!^NdMQj1ckczo5B!gSw?T`XixYz13TyJd7am7lo3ZdT?8e zgz5-I5|xszwcX!IpqKWg5uB$zC4~AO{{`c5b(NNBZ%GrhQwcuv=myM6*5pZ4dOAB? z=sgZ8Q?an$k(grH^3n=o&-0p&MlZeQCELOLJ2BMmr=2)vWti*g1;HSBckBhTcMB2f z&vZTZz*7;Cv`Wi9GWPy70hx?n5lMvro-&vD9&Sq6J`(y!0D;2jHb(R@I~zkR2Mns# zqx3n_1i@+w|I_-vG?+?XmD?SgpdRa0^9}q4JZ<+1n^>_Z>-lX2uMOT2Ld=Q`Hd>++ zTuH~mf>q(Ujq^#qCPpkO+lha99+dXCkkZB@oLE=xO*^u{10FLoQ#VO)DeVHIw?C>m zeD*q|gy0~!08lZP0dCoyB~DqeQoj@QTjTTR&)iE@zf2Z10O#nKIi)Rg9isq!eK9c0 zhlSON&*9m#$UL#0&X{Z|b7URGoyylrw!8bqoe2y8mu3e_ghB$&4*;}9f{8Hnz zAXDO!(mWefcq)2cGy{i$2NS@I@`?%yKk6(5$EoX)TkpxQ`ab_otwkc?+CHvpi-6+A zy%hC)|1(!ZoND1#Y0-2DO3NctqNV1I#r4K#>PG>uRTL~7wqC9c-(ZkV|X#`M;KXK>nzECNug1p%Lq7mS=Ufb z7n@d-2tWn&1rcN%Q4qIP&UTH^SPEs=31UzbzUdXB8Zq4&AY$QtGPBa-(3nm@m-d!e zeTb+;4#dehLlRz&73#F~_2DX-?CtGQNxBe0&yF|3!xWUbF~ij;eU6KIJ@b>+8oq;U z0PThZIv!X?2;k`m48;u4s9&bgDK0(*_KRvMRe;5KEatN~5IUcv^idN?_B--`IX}ud4#3s=5ZKw@}Qm*vH$Mby$0~8$Xe(um(xAG1DnOrqiYImBfMpJ7bt+$@8Tf zD2ktw_pFIWD{vPU7VPKDJECN116n`FO0;Nr>i@EMgoS|?6#>R_FRWjYApccL=l*%xPJZh@h*T(c}A1N2%W{TMFi=;a1;yM*#V`DD<*+V z0KJB{3C>gHNv#-cP9ooSY2f8{3$eoWig*QF^j@lfBY<5k8&?=|_DI-@q z^_!SuDU0~E?$yO$yyUeTNKUkzASsUA`~WQk zi0u0d_EYo<^A|jC_OPFghAv=lvAJHbio%kLUl9BG?{AsSQS;&`>Nj5iS5Ru5)aivV>$0&E#p-sS zWhs%ndvGLx);2TUwDJg1Yn-FqqcTh=i0n`kitc}jLUJ42)&?x(KqD$J$f02?sJ^B| z@S9K2ZE7HGshh6O9)?%Bjv3gFIVfp%bagE$bj4;b58^K5ChqRugZ1nGSb8-R!iLzp z>h_9H9Qvxrj{0U=Du5XtJ|UrEIWcR{r+_X&p1xUe>e+fvIu4K(zsHLWbC1!Bozdx_ zKBlk|5|XnWm{PHLl~{jI0eS?6tH`!MyHlaq#}!AB(NT~1A1I2)lXIELf#k^zJ<6Q3 zyvitGN{)=goh-LvW?&$y0Q@KV!w1oQnEEH@1@hW#ar*Q15vPYS=8v#qM#1J_a{&5- zh?jjNILAf@Fr_}dB9^j-2C)K@VjTDdrV6K4tZ^?jwp@Z?+W_550Zt_u2w%NP{awDh zL^YQbn;=m3ILj$MqqVh(<>{uKBm0Fpy@Y`fF6hE=+pRU)rTLF{kATvYtJVk$+plw3 z!iqtUYNehyUp$T=4@&hjZt{OG;e@}qyTCZH1fFIZAkq2x*x^-<{rQP(rqq5i@?Af_ zIgr!Ht5W>R4{FYu8CSXBIS?zyKW!6UCH`!!`-x1l2y0jd)AlYA2S^0gn;AddGD>#8 zs+kAACLV8qv|67pWhEH?XyO0>CfEn}{D9_y(fJH2!McI@#J%J&hzh`8d*Tji=CQXFFGYM^@?Y z_!sd3*umFhgoFoG_*iYcx1tdAF#A$YXHWY!PK$v=RWadHAU`Hv=tT#}9PDuv!%r5#z# z&FKn=U(tm-A*M);lWz!^jdO1JYW6mXr@IJTo;4^|eM{nDmyfyrzPa=Yg)>R1WB^h| z7-kQM);~>vn1<#pgfSFfN`|QlBb?{qQSyWW;mabHK^6yk6ucl*KlrO?ZAo3%JoaF{ z@ez9a##5aquBAZDN|kV<>60*iFRqkQECL%!- zXI~4G-5vZfDJv_x2eKKl^4Ipe^%@qY-xO%ZpYI(Ud;|*yvdO1#CmqDRs*dS>eZ45q z(r0Ez7G48H1-^6f^6H|Yq1g`e85B#n?YA5tK%mcS%~Ak3%BxNwa(@5Sayi`t5LS;_ ziibEjhS<8oY}OTSi~d0SYJ8PLcQ|6)q5f58MruM_q6^Tz7`RN@qmJd9BEuAHc|kvy zvAK|=Y&ttL+}xXS8!QJMYWUqZ{=XKwpROg;|I9gf`})FPs;2Y38XUk!kdk^tZ;cx> zje#KBjMNFoXwEm2VIMoKWrJ42lM|)R*50s&yWB;iOg`8`2)dYx(p0VE-L?-+!8r{_ zQ|w?uirI6xepbDS@yov#7Ty*Xvd6?>SZbNY8qM%K8u9toC}agIBU@MVb2rpCP^7?~ z!Bl*jf;y+$YS~x#eqz`Dfw~;mVU@oD9-x9v@qj&QqQQ8#v2KA;nubWqXTAu?OVY$mdwVTCf-5#?X+c4 z>_@JIDD*m(9>?K{0~$ zUZ2h1my4l7@eR3iJd6K|MSS>xJv20gSkS8wiO~d<7X=Dx`=n0k_>+5fGqpr#PY23s{>U|Cdy5#wv+>Al*l_w!I z>a-PUzDW7dRy)kUhd4TxbYE^wAh9d{5zCd5r^FOe2DV<$mypI*u(?q9~zM zgoM_B@H1Q!0XZ7fDX^tChstHOIs>^gDlMmvG7*P!zpsRpN{obg7ykid}}gF%E%lYumCO@pv1NNoc?l((NAJel9y?R01dm_*m15w5lE=t_10n>?*E;9 zUkPjLEJhrHKIfch&@|?gafj>WJ4h3MA^lNbG6y>>L)h(syolTCQ<&JHHcQ6XC+T?$ zxfo2*2Biz04Aeme^H}8+PK;uULU20*>M_s?()@~5Oza&_lAT$p#PiudKb!b%h6d$h z6VlLP>68^$LT+T%{BEAyu!D_}+hwNd@^9;V zXU!2%RJb=Tkdi$KqT~b^FDMG^97T`Kll!RNj8nGu!2bEy?d=5l`$Cvv3aV~WZbVGX z0zfqg4e;|s?0!e~z^aRTsg*YX!>Aywn?8};IRGt(AxH@!J<*hcJQnEoA%J>`Zxa%l z&0huc5&U`6C>Q-m<8Cu!v?eLfkU8klryx@Ch-I6m~vn&qP`jzVllwR?0yne^d@se zhlN3T9uoSr78_M1n>5aMnhpo#Ec`Xp0lBo|q06ti+l0*R>S_5wPI9l>KU z<&Ap0v=pt6p+p4|1`T)>Xz>(sAln(;1LTJSiqT@j!YVMMd8eAemkGQ*vr@ zT!2dlj5V{)gMuYOLKitK1_t$yx(*^0euEO5Rm(ZTBwV_QUEu)a9^Dh)Wwz*4Yc=`O ztKC84mUh!2NqjL(|fRDbvaxL7}maBJ}}bSnfT zOg}TZou(abuQ`T)q+?j3|D_JPbF(sukC;+cVO4yIS>gPN;a`y6Kbz9Bn zE(E*^m=`hab*?K<014b&STn>Ygi`Q1zSqa-E})(O=>i>6pj8T48@yK3yi>U?LfqdTn`AvCL%_ zr&srDxc(tUlJ*JbTSZI4^r+H@)gcz))F5nzu&uvfmm%-J#FUJau6EIPT1xy5t$#O6&`WWjt zKPp!6I3hfp9;jfeTrZ_G?^NUehysK-0c!nVAp4rw?-I;1@cI%jW^v7s8fXOVdq5(* z14M}K-6=O{(erqL4VA2NimrI$ODGsI4D75V-DJlixY&?MZ5nVr}GS1+3XeplXS z8)?n%D&=9EaYjJ?1B%eXoeTwGk_wiqZ<1fzk1BCEL1z`#XB=o25-9SKZD zCSXAl#*>GKH7J120lsowu_XzQ7dO-efF!H}XSN>SuNWZu0Xp{`0VW^)Z`!%-g&i(r z)73>RYye?}K_`#_)G?L7Y8v>gTDS^W{%t~NntKI=fi9<8*PHej+srSgY^Rv9-1epErz$JduT;IFsl$Z&^SPaM5? z%<}4LCkB{TB|X;2pqUkAFz@WqYQ|X3ko`3^S()}pzyKA}JXd;kh;kGb$hj>*0Nr2D zQ&^V)@Q=}%HSbpA{_fw-RKPbB3{V3OOnhPmFk2$X*i>vMt&C#<5si7#d(ID2%YJ72 zvho!R7s(R^SMSx-4Exyw;es#j1;0|EZbS^Qe^jLia`p&NwK0?9+d3$1j(^0gMAB6> zKs{X!q&rQmZKFY8Lv_r~Qp?-SeuIqUMKLpP5lkv2KTVBKtl{>?a+t29V@4v8gutu< zH=$4!EN?KsY_z@4X4BU>_!vDL$&?S08d!Mjp=NRLvpXIZQ;9BN2#}@m+b{lMpPA@Fr((9Vc_h;)#!5u{K1g{<< z43}_olhsr|E}@>NJ9X2PO6WYCq%D8H3};n$1X~sK&(!5aR&`GBUhLAHO!FOJzSu_& zZ2x$;^E;y?^;PApE|! zVvdaFto4!pgn$6$h7_kutSuLqdgu7i#Q+yk3Ik+E0RVadl*NLrYpjI@LZK-jmV-Nu z@Cp4cflZ7F)Kbp-iid=X>@)wY)N+rhf+93ueYqOkGsSCraOZQ5g>wkJw7K`s@rO8O zH<+dYxSt3_uw0TaO5zfW5R}(OoaI6J-G%~76oSx2?Qml(FR*b6<3hjlC164?%ECW8 zQkw#_kv+N#TqO`(Vb6;@#&w4ay-9@=>>DHM#9~1dKr?2j0+h7VgS~d^l7Iz$`WI}0 zP&^0%IHg?V0r%r`ZD*@-VU5}+cV%RTaZ>J30)xK)iQy-f8pqj1GXUq!LD}qC0Y6$~ zR28}<>gfjjIhv0C{%D+)fUiJ}>rLuiih4kMm8A}7utygS2#Ze~@c$MKD=4;Z-|hzH zAY8z>!8;s$-vP)uA*TNUV=EEs&{nG^hHKyti0;d?JsbgtIXQ426HR+tTc)iJ`$&nZ zGJ2&?+2A{9vB@0QQC!Uzp#WB6EO5M0bfMb5AVs{O6AchPnD&f_>@k7v1443+``@3T zG1OB4jDrW78>A|F!frb6?mz!$h&2aa#5TIG4+El)Ip8rSmtg^Ze?)1R)w7HM{{RoB zP{5>f^)WY_1|GKYO@hPSVCI~oA_2wk98&dr9X?#0||MJ<)U37@4fV%XrLwVXT0$ze{ z@yWe^eIO5x`uC&E1#r5I-umLpNq4@8=2|GM!{DjUEYgtV=Gf)w>64%y?Ry*Jla`An z8h5mh*VYOzn6>DO46PIHch#RQVO>l`v!*rk``A6Xq$HJHR^01KYZ|4!#J_vXx}rgg zew!$O!c4zR?dlmcy^0sxf74ya3tP&UQz;-1)bwfUc8)^8B`g_Zhh7A{rqzBsjI3 zfqiYUx3>}{PcLAgM?~^6Wmegm$EEWLa2f|B_5J;Ymo&nn$CK{d9k)6j)>p|Wi%_FZ zTT(Df5FK$}7AbEqN&>puP}LV&nqM&$?h6bOQ!c4L`EEBNRVTpN2e%nXJ=&8HsVe9TerbD`Fj#ea*!`RF zBYB*8i3p{weB$il$6EQSpPlmMmQ}poT1BRCA1`sOkLHmTSnVhTQQ%*|)wb*y*z7K& zkp2l@$o9Jqa!qCpMhguieLd_J*0jPlQ;m>qi%WfwSSkC;5~j!BIqOpf%on3{Kx$`T z##4PWr6S34mOS$Q-{H`MW=HE~?2$d2vCs@hA(fLO4Q*S=%oBUrR1pWF08xGh>pI-0 zh7BP|+91Vzfhq-#Axdn`yfJSPk9)w#;sBNk;hM9@ZAtmIPGjYsgz)QMmcQ-)S_OZc z>q9P$!Uv4*wpI_Y)KmUWPGUAJen#?ESM8hxgI&4W;O*mEDH)w+ zoLir!S-W;GUooez;(zwODzY3*iY*DwC#tsL8)DUk9u+>4q4BbyJeR&YBXM3a9Y1iy zTV}AF$9#BGFeBi&$|Kf$ykrvGR}Xbv^}dIQU_@>5-Wg7LC9_g=p`1L-*K?D7E?w&S z@UoWiYZ;ecQ0ziXa!xXlHb1%7ourCgt1+_>ywV@nMze3c4);KfM{eydCs&$Y;q_r# ze_8*&H8W(_VsOi(x7z_g{VSrV8g*`Dy1JEW>E|#ZgM-+Xq7&+(PvSPFj{L_IHIkv` z;+<|aMx%YVl;yFBEBWH;jl_S{Pv;<-?F8&L^qngk8AWeH^oI-YTqh^-@bmJ%x_WWX z=m)i(oEL04&0*67M$Lt&NoZY~U8}zuh z9?v_Hd7V5k`4J{^THF7tOX^NdKyXd$T@xH8`{y^{yfK~6C-oxH-2a=P>C!phG<$oY z>?wG%;)@qk?qz9B$EfU^^n1RXH&0Fu3q_0b7Pr=-|Ia{_kz14~&gh?3^nxmK>7Te{ z#eZ+CIAsL@)i_1}K8j!y^8VNm`=0*8XQ!SOwQXLzx8z29x*RUq^(P4XulJA2-q_u5 zJyLrn)1Hd)a|h_PBpQ@gmP`_wZ|&6Fs%3ooz<)z~$)w3c3Ce3p;1$yG)+y6=rmsfL zMlN)-MI-s@)?5JA9^K_HgZ07rG~IIN2!DG#{byk8K1(FGu&S7P12}@&E!z`oEh-JP z(}0Xo6K%iVx9oW}3JBMIpri9vq`rT3{H#?1=NS`H+o2}piL@>}{5+hV;;SAj`7ydQ zD~UgQ>*lS~{Jf^yJ;v629pbMzVg!RMNN9Ah_vn%r?Cw(zTon0ntN-kj3fanh zlUwxq0Fnq$?v=-Mhavzg-KK5`Gb`cj6hQE=_$nt#Yf?V zf8Nk!PrlfmS&c8`Gqdv}3V4jrLe#iy*z@kCu;e5!-hBF!I6Sv}!eU0i?YGKbkIkOn zHMe?B)(HRVl|S@Mswe5*7-$qjuP!|Y6cW(h$Md|17TV%3CJ(4qUsHiXbtjy-2dr2f zXO(%+&VU0AQi-^_nsw;IJK^^}lr;k^#6dQ^wrn&%-tor+Afkh*I9lZ~jy=OUJWmPq zdm29@V}-7m)6T<|K8+t#a--?I*v2i`T5ck}KME09^|#{3HQh?BP4_Ad{Qj9M|IIDL zZ-cOUq?=(ovy5iv7xAbW+75VJ!31k)edd>Sl*NImSx2I@*6wygTD|2rrHS$9GqIx1 zP|h@&(?H>xyJX9b!hdgT-J657h4pSD;xp@4yx#AI-J@fNN6g>=F0qpRmmi1jYu$Ut z!!sgS<`3$j8F?xKzSUr}b|Z;CcxuI?$*JtkjpD?<&P(dFcV25_Gt62#tZO(8svxmsb+|UPrC=x(v!u<5H@{YX0$~=rze@9}i8QQS5zGZM$(+Z+0|x&z((i@3a!G zl_iaV3&`z3vPIJww99nr%g4V%71A_Hj~==Trv8X@6qDg}JZC0qaC1)4l2;G5li2=a zTF*?wb$6JFYgNoN7NYgUnLM^gOu>cV#P#~-Ym;pGa0xdG=e5A{n?V`TH(cpsOBI(k zmS%nUuigL3;E}kmWq%G5?hJnUMDMr2ysxVM&`8S1sp$K-#ytVz|2MzKfM&v|_!&wk zS$lU>u0pSr1$)e|$m!zkE+*xpqAje4Fl5-z|cV z%}uhp$a|!-#?ClHWa}B9iVPKPio!BbLhCpY%Z^TQlOySU8GGLg+c@(@?vA|cm~xo3 z5@2%=C%u=Tap#u#l!0S#xLTLvV~X^4XZNQf-I?3XLZg}j{P=omjmLpO`=;W&%96)J zxjt4V-Th9PS*jLm%jw5P>EUZJDxMFRQd5YjCR6Ciqq?1rD{|hFC+!sX5@073A$e5o zTtAltDQW4I?{64trpH4vE&Hdsc+cxP@oH8-WiyrfD#!RZnpV>&xqf9=E#2w8@5!Y+ zvXQbfB3ffP6?Ty~L_F-i7jn^5;EtZJDX^HoYE6J;y$@G7aT$Lt7Wao~9nlRFzL*Pj z^^%t-Nk)zK(y(kvUM|a_)`FgzY4j4_v3BydVjhPPVu_3+Bh9~_Lvp}b-D*9{a;o*c zu>R`oSQ=iS9(6yl=mn(2fz7h(26O4Ago zNhD`m@u|1r)hqc}EVVipCv|H-JCSL_k;%EVfunKv>Kd3od~fIE7kHN^FnB&eY7GlI zK6iU(ja`1|jDCBKNU)IO%d2SQ&)h0rw|7$g*Xd9PcerB3F!&KRMpsQEXJbEjPHJF) zdSKhgZGCifj?eyQLswL;55#r9I@_|A(Qk@(c`DKkb+X5p5S$L3$uZ7G(SiUoidJ%} zmS$(kXZPKgxQy3P+Oe!CjwBy`S0-l$px?!l1pG!=tx1%)g$;+5!y{yYl*4w)&A6IjdZtUiJ zedS7Nuye6xZDb~o7K^)@y}dp!{;!utF4fX>IDHOFfA?zAto~e?h_#A6YT@$IXYkw+ z^{PMQfHNz7NlrB_zMs9mqQSyr05O4RJ3rsgI-W(eq}^XP787Jug`_w8tt9CCb{Vgq zECxgNhDXrt7U!;OpDEFM_2vXo=+Zhno2LLlI(-0J9gKY@H47Qp1m|6D>y(~2YJw(4C^5_yEhd|g?xAVR`&8Odj%b)$l#Uz$E~9^!ix zIDPF4V6TUo8@OGq^!5mElRBj(4VL~Hu->_HHF0TqFT86?3C&+gP@FX^znJPnH>CZy zla&LcjnA4VGUX{b2mANAlpWNP(<)IyqJ1fx%3Zxx!bW}uS%DYv{*HT}{Z9rC3&ewT zb%SoW7y(XDyZ#nl)cGh z<_dM8TW5PG)!cmcal=k0-nfdw&LUFSaY{5V*J9M_{ zV!7H%*!!;!^7Z1xfs=h-20?Lu`=nRy>97k#Q`AREME4-iZe=P;#7;$^_+-$3cP?!? zh*o{1KHY6-UGc%DLYMUUNcl(MwnI-3Yv*1R%8??So^e1TEk;KaeMpPXB1xUv6YX)+ zo2T&fHS>*&9_q=-_>y95UJx=YtRvW~S5E)y<%?BI1C`);+A7uD@w2JAUn^G}0R_#L zy443WX{P@#pC>qsB2)5KcDM$;s*6;2IvpnKD!3#sWR1y!u9Q!AMJb^(2gd^WSNZ4Y zz3J)U#&4{b>QPE-b@-`sai$bJfwptL1B0%EDYOGenE^Kb$MVC-S1Kagb4kf}`ypr< zQzwB<%G6{#7_2sibT-vr){Rq?wJXD8uby}DM~VCF`7?o_AQgj>462js%Dh5RpMDf7 zaCYKCNp+PAk-p<-l3C(6ZoNOyXjAw1tms-rIWT3bux|fPs@90>%5sKwzF?kJ%M8`b z{UUN(N@R z`sn>sQ3sXnbsz3&uWh9*yEWKSre3Z^$JzfeGcDWuxcb?Ht`tCotC2&SdU@gk3S}NlRmiZChnJ?U2&~cS-8HN{KdDn+k`zl-pG0Ew61)~Tv7gIZ9L6SN)pwj# z(4$&YilP=obvs)(u5KY+l~f(;SG3ma{5%ABVC_B*EvpswVv&-Nq#957*f<}Q7@w9)^WvEW-@2Nkk{!33lGdGZBTi`T>w>P39Z zFAD>}tzX@L6DBZ{WPg@D|{8953<|T;;^ZYdt^Ze|~9n8y< nf6jTq_McP!6S4o#66P8k>w*DYuM!2rq@1FxnoNn*t9SncAim`P literal 22801 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 W-FKg^bGCg43Sv)JKbLh*2~7aN3?W$n diff --git a/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png b/test/interpreter_functional/screenshots/baseline/final_screenshot_test.png index d860bb73521ceaf40d4b03e271b068d5fb18fbb7..753ab2c2c6e9492950600b52b61a8241c79ddc53 100644 GIT binary patch literal 17033 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 literal 22852 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 Om~LLP0HLmWB>fNO{2NjL literal 32900 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 B)?oku diff --git a/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png b/test/interpreter_functional/screenshots/baseline/metric_invalid_data.png index 795f2f7c832f335a1a1d78bfd9c820e1dca976a5..e0cffd065fc4ad31a2a7f09d7f58de66722a83c2 100644 GIT binary patch literal 1806 zcmeAS@N?(olHy`uVBq!ia0y~yVCiCDVASDY1B$H4iCDcuu~YQf??lv5P84^L>lmdNFQbpc^O2E3Xg`#Xo48c4WmT?s5Bfc9tTm~ aaE4jCpDjfCnrb^JYCK*2T-G@yGywpYYsB;b literal 1920 zcmeAS@N?(olHy`uVBq!ia0y~yV41|gz^KE)1{9egI&&`r1G~GYi(^OyVv%M>FVdQ&MBb@0DHC3!vFvP diff --git a/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png b/test/interpreter_functional/screenshots/baseline/metric_percentage_mode.png index 580889bb7deaf3f06eaa5dcf06737370555241f9..14457f0a4d0ab750bd49fb09b1c1e946b3611365 100644 GIT binary patch literal 23705 zcmeFYbySpXzdwo~pma!gt8_Ogl8PXrbjKjgfOH864BasFqo9bC^dQpR0@B?*bl17& zd7pRhz1H66ch>pqth3f;&0?`&=Dx4{y1wy=FJZ6Ll<;w=anR7v@Ku!MU!$Sj4M#(} z!*ve}{HD-O=Mx&5fr*Oz(>ES>wwvyo3^{qB{6jkgY%S9exABwPqia9k2EVym?Van1wQ}F8I(otv z-|3kD`HEkng8lnz{C|J^dn^8TAN~~)|GN+WyAQDce*}RiGjlvvp4xSLF--YH!MEqj zi3=rJ9Q;WC#E;&;5KCP~mvHWvXI$aRYM&VM$+qPfQrS<|CHX}PkIT-atB>#D>s%)$ zbaeV$$o$fu5^WlFbwbw5TAy>AT9%VCNi*KH{>-(Z70%Z7_Qkce7yD*y_rC2hR@%xw zE&A(;{LTAA2raSqNtvxXYeTm9-|KbTk*KfteDoSYbmtD?Yr#F}=2$^gULJP5G#eXR zDThF~VJ(Ze=YdcPkKyV;Qo-`^{@1_!pQ1mO>RgJEZcf18isMkHFAJBw5$lQ0B=$MI zR=+x#qT(^Gmy1wZqKfiYd#4 z|GXsa{(8iu2S#e(B>r#IO~GgM-K5iVwt`~FPSEniDWOAFTp zSEdv zZ*Mp*q$Me>(uAN=_?u%)k`g{=59=;ul$@P$`d@#Gl`YWr2?48!uX+9Y^}DH0*j59I zOI*iWBankjN2c4vj11LRneDksZ*rp|NhsVSR=KkmAqKSlLp7S-A@xGALRP-c=U#Tz zMI9X-=^2b{Y{6A@UQQ==LjA8BjO^JPZpn3LU~%RXWv-G)=Ep-JoW~4&AA{zuKa{`m z_$Kv2CS3`lS8BseI+qJ}|Hx4taZa zNx>=!i&1Z!6idv_Wi?=9oM$5U7og~(9gvny{C15EOAg*T7*MJChG_m zUJ*w;S+U#B;4i=Qc(?A5eUxKs3Hj4)x1)5aA)bE9ertb!pS{)_dHzX$M;2BfCHfa{ zowqAX!RGILjy(0-=vr`z1)N>=to{QsTc;%QzVy9Snv%FUH>GXM&K?%0FF1rrR$W$y zL~)U8`(MGbb9{fz3u4vrMgIBo#=v?%e#M~1Iq9csU}rZuk6yFO03iryOzoc_K@Vka zK5B8NfeeYTh};@;d{zBaus=&Wlv80xwlgoAJ4byNx%`HvrTT|wkH}Dge?t~xD9l)W zutHbTVH(z!RAqM^;%Xc`TCPS`0n>SR5!kh$P$Vw%sN<5X^nEaYVqEIjXs&NP|r$o^ffL?Ivk zx4k5!&AlAH?!p^w|71|-hTrAG&rf0frvg9qlS_3yt%T15tB5+!1U*-p1l{(i?9R0w zEoNq_mM8MgxdA~&>=GAJsVnVKI(pSR&$amdnlCfNpz*y&n=sKDCnhXt{rb8K9<5Mf z?;nJL;mkU_t(+@Q$PPTyDy(jqdJjDSTX@&HY8DiowL83TXHh%uNuwC^Q z!$Jc(Mb%z~Y27J;87K$H(+AKjtWk1Ag3NUmhoraXxbn1N%X~5-A{`3tg`X3rf`s8! zR#SV5EDET^MCPhDti?r+6?F(ANWnP{ZM94=xLlstPpiaa4KOnouY|#tAEI(y`s&JO z<4-9ap?3bqHnWAjg&Z#Z`ZP}M;>B-ZS4haLTj;oTf1)dq@ft<$n;WmiZN9D1x3PKL zlUxGn#%5TAf$~ATrc>&S4~14M$0#=8m&)j7HwRybMay((Z;29v1d6`c&odVDX9_V> zh=`q;MiBS(8+togmDI^3AKB1NW=vLQ{q^L=C*$TC&vvwUEyb8%>6G@BZE% zqo8U!KSfP78Rr-gsHI2m9!wK$B^*C87|#Z$?^EeVs&PrU_>IB;BKVNP?4ef0J>+Bd zRpyDV(j^WtukE`V8ns$4(}IP*dhCBTKoGTzrB|2wb~wi;yTu8GIqZ zUGwiW_FQONkVnqMuozMP$d?ato)-T!Lybvfan0%mZ7!Y`Qn_3P_$Exzc^Oi7A# zkwSLRevx2ZoT&n?xock%zdA*!iD z0l|l|%e%dBk+}0i$UKaS!S7?OIE^ns`Saw5YOVb*Jv=;O$&(&yF@k5)1l0m(5usfufuvj{#8FY+paWE@k1**=r`F5es2rmiz1+8z-`~P-2jA0{ zdTcmPcj=SPb`(|`^Ry9Pp4hb&!r>9HrDjq4w!*9PL*kQ_HF&AxfEMk3b3{uQ4*)Z% z4Nm+>9#HlmZ;SMTL9E1UWo6NCM~w%vWNl7IL<+43n|2!G&Q*|=AFK^1ji+Rrx7SPS zLtAlq2|5hvByTva-_|%QOixVhsq_@qf#)SM1E%eVL0L?Bx|I@mN_G7=$ozIt8mvFIXvb?YAojX zoiAU%#-q?Bh}9+y$0(X$79OLYs#DsiLa&taheL3`9y#y&#Cc>CwKU;=hUS+Qvn$w-atCTA6h2M5DcLOnb zen@e4p3;�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%0D!`SK?hshr3L$*aagIV( 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 b&~h=8fy36? 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 diff --git a/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png b/test/interpreter_functional/screenshots/baseline/metric_single_metric_data.png index 916a284433874aa33d618c30dd8751b32819e457..c4fc4d3979152affbe4386deba159c20c9e5c5a6 100644 GIT binary patch literal 22004 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 F@i*ZEpSJ)2 literal 29643 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 literal 22801 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 W-FKg^bGCg43Sv)JKbLh*2~7aN3?W$n diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png index ee9182a654d1e8590a02aa23d87abb57144a51a4..7b96f3ec43c7ed6cde24842fb49ffd41f02133be 100644 GIT binary patch literal 7054 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 W-lz9Dga5PXQMT{eb_F{4^}hjykH1|2 literal 7049 zcmeHMdsI_rwubMF#QcPbiK#bw1GrQBmw2&>iDNYF!w>$H zXFB~|@}-BzkBl!ZO{}c;m5r7PMhN0K@$EL*-tsR`O5Uc~AA9!C<9{m`RsV3yCA8vX z+XhFJ#hF3u-gfCn$CLPkC;zp9a%sz^W_u>_Ak*5BzqyQja_BN^n_S}Pl_UR${OR_t zsG@#{^JRB#&D?xiaVMzp;CNkJ+Xn(X4SaN60ilLp<;WeCmEn3YmA}41@W;OzzGV)E z?+d-I`X>c=_6EcELI3}?h=jZz)j~S$3xlM}5?<~uhO>k;5vu4`E!5);avqOD*$^o5C8rlU(Bo8TR)1R!mei$%ci2YjEgtY&9t(#`y|~=4k^DG!Ut@ zH03U=K|{7bCg5igi_INSSm8|-KMjiHwiJed^RvjPtDxIjrlBj{Ym@Fw7vs0HX}5bh zf2!s+ImtgV=$dfs0}@CwNm4#8Rq=T|(SpJ#QT<`v^b1d|4x}mNxG-)*h|}%fIq#0o zmVA=p!ngyXnkK}UY@lEOM^|Mo%~ayIV=_j*RTg$p!il!3%%5=v2cAz2+y!K23OOlB zN-gJJLjHrl1+(Jl)e<|jVH5B5bDE;V81%qeugOA9LeSJwEh}NT6o-;YNL2Rebz=t~ zkSeOd71#C?Qf*Sga6|Ia$hD%i8&&G8Djlls+eP1(B^f$bojO^&r*m@r$my)9;nHp` z**C{Y&NDYE8dW4vh%#_TB(2c+7LF&YbGx3UI4@oSGP;&u`f{EYrG8zY~K?UV1XOVCht4^Pck>CT4q5nm%&&_R$G zVXZ@m3xZXgFPqklIT+5MM=t0en=V-FTFJ=2qMI*T_E`1RO?qvQWVA_z6tQq4f}lwM z?H@B{FmWm@%9I|KUlDa=c_QD#<;75jdV2NI`0Crv7gFl_(*xf#MR?pvV}v`C@_GwPOc ziUCX{BvbHIR>yLWvg6j3x{jN2-_FI!f)^XLZ&0gr6J7tTkrCibJC(p33HAZCIh#A@ z&TSgm)HkoUlUdifu+`=BdBRv)^CQ`j7ob9yV4y(4REK_~v~r zgrb@Du47qiGiN*h+YrI@>zr`CC)4j=EL@&Nvd_hgyi+UpSvsr%S;0pz{a5t?sVSJ3 zCr@;T%nb)FuZHN}MR4+rwB1Y(v&iSuMay@QE&LSFaeZyhX3f=L&F9O-jD`7Ah3Cp& zTx~AI{M$`^P_x!<7ClWyqILJ}kuT$t^UGq_R*KZ^FZ*U+ATrvvbhlSm%-o9G^Ds(# zJfyE?^~JcBv3k2`v8LiJBdWK8z51>x#wQ->4O-Brm%6oO-FMBp#~BYomX+hzbJxo3 zTQ3{)bSkb=ypHTMVJKm9tSgdT?Yb}b9H9kY8WhPTXBQocwb5ruWS$v<(r-&p&aBGo!!?7(cajO2mr`S@LZmb?G)HYXXW^ za$DnzM}3?KWC~V_NIjpXj)1Z?on_2~oRQX+dkdK_E_N*hb!v|#Pt=cRwvhm`Z(_QgyrYJY>#(GBa$rSaPZ^u=awSBV__xCcl+E$Y-;Tk;oRThBG+#A(S@{Yd>%+co9*Ee8O1ArK%5>wfpb>nIOsCuD~V2ce!@vL6ewp)th z0+7GN**yGB_>gt(%m_2}c1laufqv}_x4UgnTFaA4crhg^l)ATP7gk{VN66oe)Oh!s zDV&c_&bst)kwZ>9bE|NxgVZjRi$wf7BQU&G1k1h7Fq`+q`uOUFz;Bmr(AEidm65%q4_3|8$Qv6l zuOd+GTbL>C{Q9Y!S`sVHQzj9LyiD~Z#%XitQ3@7&?#}6}`_DoC+fo$~=d2(Y*;IK# z;0~;c(kg(xHl-%rXsDSC^HH?DW;Z024Txe;G7ANu0)ja*je?6BKJpd-V z!sel9Q%l%*Am(wALbwU9zC`wXhp(`z^|FTR9X*$28ZNigVeIcs4{N0hWWK={oAHESCMt9xc z67vZfIreQ_o2Zd3T0k!)nElKvulINVG^$?pzL5d?-V8w17 zLNQ|(kD1xG^A4zI3ZsTMqi+09hE}q;Vg~hJ11F?gJP>0E|4IQbIz;{q2%Qrl5wNNd zr@Y-`dHecRrMNVr1;PdT{3}SpK@sk?$)2|4{N1mu1W(e34oP?>?1O;jAo(}18v>k9 zRAXYKXWPA24r%H`qI-5R(B%j|ezHCQNo1N!Bw4nm>@ibQ@BD}!cF=9)r(d~Z>``2p zxW1`&l6yci5VOuzB|?^w1=d)5d>YQsD*}8<<6X6Ke0E}oZ|61pe2?V&HC3yqi)rD* zr7@nu1@fy{I@gevEIefdbCgV&`SHvse-2})262|) z=*W$s{Sns(Na~VYbE)FVM|;4c_m?(}_uFV|>gMi%s$rJ6f-O?_>#nbk#m#1lD2y~> z%IknN7>Uh= zqZTbH3m(=L@ypY~h#6RC?5m(0S1piRhA52mUnYTrLRM1Ve>xoU!%p5tRp!^4okpgg z+r%+59Re)t;rokbA>BpGF00#C6~0;=HMwBgx}@->z3%Oz6H8COEqdI)5NIHiSR*{@t{=sG6j)~lu@8*|YzdTdXiGFSUU z$S=2C)a3;;9-UP#tJGw5BPj421C#%akLik87;{7E3ohd=@n=_07yWQ01W{heRgZIw z$M%e)HbmTU9^ntM))(JeBB(tfH+XJOw80_SvE5|?PLx=(ouy&YLX8&qkC3_e4tqQ( z2YEtc7|&aKgelB;nFJ!#p_S{79T1a-1ssyv@q|H^b4K6?Sdh%zA=(e{_>116@7XKg z)~&wpkMZB8*nuqS1JN8g14^u@6BpaE8+)mlSsOFQE-IjPR4y3`O zWVF(IN~GSKkf+1=&QW!@S6W-*&smv!@=?G6i1DDjojMISkwMjnacB0{Cwgz!jSyDW zFtR!MC?~gTqECI5NeZsDi)W*JfJKGHwGdVX7?H%8g8 zFEx|`tmg=gu^Cf1P&`2oYmRM+$1N%qnAyXX5iK1p`@6;)L+pmkj!9*_4W3leY&G5# z#WgSH!gs8d4G$I;zL=OvTv*M@ZRyB@KsqP8nHlp=q@435UPh*PCb>cW%%tw~+j7r2 zQHbGp;-d^44Ucl!$#g(p+cP@IuuQ5gUUs!ThInfQX3c;;$4HNFhOd$(_4DOH;- z*+hvZ>}b<=Z-{Dv1!coLH3#qKkgB*@Qs%eGeFw@7A$zIoMJHN{SNFgPaScO$O6XA-kR&6Roio%Q($d1QC6+M7 zjChkTI;40fB8=N4BUnkmkb%pI0<>`kf&e0^G_);>dxKBL(p`-W?t7z(zX@&MK!aFW zTkRlPN1ub6?9f2>xVPQ{S~U-V*%k#sFrOnKW3f^^LhLWDmwo}<1P_osZ7FG%ymxHV zFkD;E5WzKby**~Mz#el;S}28RN&%JHf)a|PSZnVJ;(AS^qtJazw&yEbl$|H7MT7wQe1;kV41h`t z{4_k!a*4NsmBP(&V;=*zG0zCB&pNP!KEjT%jkArfRGHGI#BfDB6IsyzA!F<<=psm0`bG?o47o?!^!14r zNCjo=Yf{u+BsG8isZtK8-B+o_hmu-{MCETF9EGJ{1=^I#YhchRZyLFV(0j(PsAi3+ zp-nFxUjL(lWfYXA?54*Ln??PD6;!*?70jd=eQzUm?=lOBAjh{4mU=XWjDW@!{OxYs z*Wa7So;;jq)Tc?^ZK9aUSBmVbNt5)-KQV?5@kBA;flH)|bVOI|EzvQ#MHpkuTEd^Z zU=(zsiR@aiBYt?{!jLo(GaxE|9kJz#VuxZArX2JWJkFcQ%YgKq4Cf({hdsJnKce5o zh&1kX2nqLJJTknHwl$52J*5*v|p`2?g!n9T7^wT)?Xn5xlm&OH0G>@J_w( zp(JK;k}G7MGgYnWglld@?{?pTO;zmTf;LFi?yugv`zZo>kv*i2|2MSduyeDcM=0CJc#MgRZ+ diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_all_data.png index 03ffc7ac7b1a58b38d4e54eb63ca198a3101021e..a7088de3849a54e103677f10d3d4224921e8feac 100644 GIT binary patch literal 11827 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 XKy)i_PxajewpesD4K>QuZruM5$4YVU literal 15701 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 G$o~LiX#uhT diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png b/test/interpreter_functional/screenshots/baseline/tagcloud_fontsize.png index 3a7315df405dfe11b32534e49b44700e67e60d9d..8f93ba81ad2adc2d9bfcd4645303607fd962a233 100644 GIT binary patch literal 10314 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 bZTAH~-nel&0XzVK!A_nCJ>K-q=^y_K?U1dR literal 13808 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 aaE4jCpDjfCnrb^JYCK*2T-G@yGywpYYsB;b literal 1920 zcmeAS@N?(olHy`uVBq!ia0y~yV41|gz^KE)1{9egI&&`r1G~GYi(^OyVv%M>FVdQ&MBb@0DHC3!vFvP diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png b/test/interpreter_functional/screenshots/baseline/tagcloud_metric_data.png index 5cdce6929667362eae577ab636fe34f789debb6d..98890b9687ac95a52ecf4a4299df070eb5e86fa6 100644 GIT binary patch literal 6712 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 mFo1ISpN??m3@2hhkpk)MYIF} literal 9163 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 diff --git a/test/interpreter_functional/screenshots/baseline/tagcloud_options.png b/test/interpreter_functional/screenshots/baseline/tagcloud_options.png index 394b5585097a24da5e3c10ed2c1b50bf8a9cc1cb..479280f598aef7d02121f8874c21c223c9f642dc 100644 GIT binary patch literal 15100 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 aHE&iz8^0!7DlG?zEGmi`4~yiVy!s!XBR4+) literal 18998 zcmeIYbySq^_bzM)0v1RJqEbUE(y1WbF*FP*-JPQ%AT5n_4-L}IM^ZpQVx&P@hK`{J z-uvP6J@5Iw?^@@-bIxBs*8*AdJafms_rCVEuX_h7%1aR7k>g#ubcsMp@~!fvOV=VV zUAoMC>jwBFp7?9yrAtycskd)b-7c?vzv+d>rk|f595{;0%kS%5i@dj=g4bP@n`=F# zW?5N;=F`%yESjEHv$Xc)FDWXj3e#Twz53+OKYpLzzrSOAP4cPn7r`H0JiN=Ni6_Xd z`q~xVF79VfzmM#i`<+RF`M|%|R3ZQVaq02@%|DW^T+I5<%k}KI8C6$Qa+%~+gKPh; zWK{pZ|G4zuMgY!mVFbI7ofuoSp<*;}nScJ+fyntoN`&W>dm4JK3j3oz6`;W6_#M5I z6-ETUaii}R^jx*^%)sx8kMlNFuF{H(u*y;Q48yN+7gO6~gz$fkSx4}xZ;SnA~4W6Ok z(_ct&@VhhQ3**dD!SUeOcy3sZG&BtIVwn5bM0MD&-!%N>RfYG!i^rW8i@Ud1oxkr( zZ=ycb=SqZk19JA0i}x?XH>XMzOI2L_J|Ji1WPs7{TCH>VQr)4FV4Gwk(-ucB%ed*HsXZib?ro?~ElxR9u z)G#4;hT9Vbl?Yu!TE?1}^q_6pLfaQrOe@mFQZ=VdXHgz-Y7)i~N|U-qwyc;HD)uzp zy$;5iCIc_yzd0G1L@C13Bz5e`g4mKuHX=Jx88u^D@;o*-!IEEZLN@S^-yeTa8EvC% z%TA(`of0y^WV__%H?&fq{;vbPHollrwmqL8ofvPRvLMx^&ciU&uSCPSN)=1=01VM` zVH=SIm!jC_JmveEvF!4PvCp@&!lLObU(a6S(^5Uy-i0P~yBO*%Y%~v3c{i84>!Cti z>%)@itbSb1^E}*^x$~1;VEjvGF&9UmRK9J0oqvKT(B3mfV4WFD%gCTUUAT?MM5H|S z_#86qDbt0~(uUMa{EJHRxyc-xp7l8V2;1YGQKX^7W?M|2)q&TxkQe5-e4;+X;wKW+ zmKFR&4Jda! zGm7WEVpF6^SA2bEcfcMVbmGq=b@S5fJ{|<2Lc{ahepeX1 zR~}u*6@i@`3S8_5kAVM(&V;2-4S29e5EUn)xWGRC!@ZYA)QN$eLBn!i z44XGP`%2aBJ0eK!xteT%BKaOFxU*Kg>-;es+Z+b^3DO4^CqoJa zUQ>MNb!KIrmn$peu4mWDJ@2|mND_th5*Ml-_v*|9nA=I8NTsu)Z# zV;{ZI+Er$|qIdHburU}J8F&oWJT$I{t$Np)*CXl&QE660S}EjKCeo3QemrGTw~A*EJ(-(1?-b?%ZbXRZcavI zqt2oFHnoB(Kw@KwpQU*phgIv;`BS{bXF~*>zslFHmIQVERHZS0NRL@)p3Ii464?I? zPGc5IUB5(SG=sGYwBU_RTE0a96hIo8ZKjF1RfL4C~ zg01<0K7-FXr(aY&BCVDiml)ET-jpR5pGKyxmz`4fu1~pp)E=ByKq~N;KaSKyczGta z!KXJ*gQ&>Z0y@)EPUU=vrx01J&QxfrXHu~@q$C$@r;9a>oLLgSZ5MJ_<7V%a`8w z`vp}qsyZPfPJ4YQqsuuYj996td&-7Q4vgyIJ5GTjAHLLG!@c2*<6vSHmiJBHh$102 zvs8=I4^SYsol4Km(lKGJ(c1WAMIg5Ax4ZCJUh!zPIZosm<$@H8A~<@=B0g>H$`iDD z#bSiSuwsLD1n_RO%|y<)^+d&XF@Fqlki_ZquFq2i<+3%4@oI%$6B7(WtzC4PX|FPp zCnKZckh_@}gs>IlpJDFk)dK7aWoo`m$8EBmcb@R7GFg+7ScYGZetoi}&z14itvB2? zjNy7EC&!$r$$8@!JV*Q*ViN6w`Q- zNz}j*y5TVvlLNV|^Tp(-vF5Kur@K3xYtaT6Wt>mt4N>&?2{GPvA}Pmht#zL@3$(3| zlxPG<)YMIf2H69_Jkg@EzH}x=$j{j=H@uk-WkxH+hYcx-}Cr zS+m)q^HFWL12KcD!&&;KiUEv&ZB-B=-{sL(UOM$u&Eicvz2p{vw~)KfPIbq7ehuUL z-Y4I<-)LEBdE`J4%WYeybn?4U36ZeB0q1%7^(%-A(O@Vpa!1_LR4C;r!2zOm9qr4j zPVIy17E%*S3jH+4&!>IXJ+aiwcw#!e9Y& zJM+UjlZF081kx$dg`yFahgvaBZo%WL`?+i%s_0@G_aD0t;>GZuOV=6>eG=VWnGer1 zY(2rq8Tx$KiDQL(*%T)!J*iY(ADM>RLclM!Gk(xiu&bw29KYuz_Q1?mo#!PP878!( zPAinQ-GY@(-BI(aR{nku<>P|*5ZQKPW0=tnQM>WG3&4e9Df4$z+(F)9m!b2pYi@As z{I9K{fPz0v*D=aRF4=VWJ zmNO^C0MLl~PO8sQkIr_~gW6_Yn3Dc37-Na{PT&nYoa;Jgk=-LjDhPJdpNg|(LmWH9 zChYP6)Bl-;GFh1P#O74-`hk_Ygm-|lX%3@_R?rpe%f!N(g^?)fVOk=*738PA^G{cv zRdQDp0Bb15H8}1LXaA)VV?<~c;|dN|8w+Ms=J&=o zluTB{N~;iKHbPymdOFkI&Cjo};y4IDHq3JoY>?S_TP?uw#JbtFo)5zx{etUFv0Gf7 zabJ(BjCrth3gf`<`YKG?tSNpR{ZuC9!MdWQNkJqXO4%Jo<@#%yuBU3Vaklrbc+ssZ`x_9|25axb^)_SwnKi5DLI!Ka6u zi-~az)^l&i?t=q89k;}zKdG7L*yW#zp#;IG<whM%~D_F^$lxS+LYPO`Nf4YY~HTTmUEm4u7tB`;>I+ zv8YHG1=LyEnyJTyJbVRN#&@T$8yJdc?`p5uyXJhpVM;xfy%%*!9V>3S{~o^pD<(Xpp@{A7OuSRyEEZwl?P z{WuQO`xu$q_gf8{z2kd#;oxXEs%I75GU3B8Z})@M)*`*&$a3c2C*$ z>`^nwnP&k5=cL<<_oY*k3Rz$BzD)4xagsY9a}qNZD=5m5n>eeJyXkl38EuG^V#g$Y z?Zc@HkY&A{`H)Ed_ebXJekpY)@roOvCPc5Sf(ZRe41-@F|k zao>j>hnNyXi7WEx&e*J!bSx#<)(vM4Zctg_3qiqse-R`xfMM&}-xO}5Z3^MPTbL;q zf*ct5avk8=lecZ$%Nz0N9er}xmrX57c{3h5{SbM^ z>2+L|;L?!2H_xUt`Qsr6(Wj4ZicVTLE3kp|PB18nxqS|=vK&oqF51$B*=k>sjE!!tv$6HM&94DSLBp|En02=-0vqg!u`bu!NLwjBHOKwqJ z3*kbz>&CU}4qZ<9!1)KxirWv{$Imx>YUgywT^D&Mht-C1SXVkxJ!Q<4M9EKzqVpIq zg9D@KHK#m_DX|5=gVc)ZjAMTLxE67p8E4kX9&Hm*no|;-aA6svs>N&&V(Ho7c5)6O3)080@q+YD50MHK4boHS^ zZn|5wM|Uze!c=H`28vZi%MMxh$Vlfez}pvOnu{G%J>LK>%n->EE>IYo4+`7&-xq;K z#FGJdlXJ_A{1}q$oPe7cb7-%1`eMTD%-~rh zJ0?>`D|4Q?`x-@F#g!}LJ%~yv{NVS3L-~TO#EeaTw z(<+s4v$pw)t+8>$czz4L6ii)%18JGHQV`_e9pmtpKOs-PEloKTu%qPEK|&Ldl9K+> zqtdtS$gxydj@Hu79ABRxmQTynZCz$lRH)a}dxmhX^kCHRC~L^eBfr%{Y7GFuxvDas z9w`&-5SE%5AA(Q0@HuF(c#$nHvsSaQOK5|KO~ixhhOKgy6h2hFubONUi)|hJiRg_6?&AvpFd;aR zIEE=C0S&Ano_wPF}5in5%Dlm;Bdha*0 zYi=97m8$SO)TagkzaeFKw&?7RI8&scZBu&6Af3E2;vM3|n|IO%E1^6K3+3Mw@KdQS zms}4G1wiiGd6DO(Qz8pl3A7&*;rSbr$=rlDBZ?HCWKkmIkq5E`V28qSw?kVzYm`t1~k9z)Ucj z(iGLICo_%eEZK>}RO`;370Y8(48{7zgW=7p9g71Iso^~>IGLN- zA!M1;L@7RXHyB?wzpmX~D%eYgWAY3RSiuvZ2)y8dE>BWxhri}oFjy+BcWBXu$qm2h zupZ6^oUOwonu|#;@0CgnyP%j-mlAmprkYuu=62?zZsNrJX#jpPjfb8VB`iunLS3}Y zS4DDc>SbP~U5#PKQP9e1HFAP6Hz-Lz_a9j$p1EZ0k#qmnR+GFKv@A^8& z&UR1?01>pZN-f|y-7A&ZYd9asGavJsW}!fP*k|;&w8kdY z4`Gk|qWD&ygZko_$!6g;<6(EQGzGE@$V=gUzr`UY7E^p!%XfG9uU-DBeiV=sg>+(H z@=em|xJ9&>voZ}m7C!!Pg_5!?^KUx|#Js!160Eh+D$|isM1dtaS^+IlNz6csIs{Vi z`~IZG(u%DCzl5WnuY9q!56gj4=hRg;ocxLoemj%IJdJr?b-XY*8riqL^G;3?SqUf3 zJ6u_XW2bR=*H6i>-!vm4Dpb`haO=ol z7;R)uis9f8aac*6wBTZjTgLM|rtx&qfeO=-7W?)#pf=+_ftZofcaYInfhasow#;CD z+LiBh@Z_jlNG-1LNZsNENNnAj$JCW->xzp&h96U=p8#YAD0RK{>P*hR#iU6aC4IBY zi{xh8mW&IyzrTKs6%)ItD#wHbO?t$NOJR2 z**TVm+l*)B#p&jR&b8*SVG&UEUMUh1Fo-=~#+BuiPrn)7t>xU{2&{`Qi zw%v&vw=%{ttHY1uMhXTH$7SNkl^$tkm?BLp3F{zGODcde;5P(Hj>=|S=&=#-!)|Sd zNG{>MG>d9$6<2F47SD8zcR3$a>VVG}g`4+WU0Z$YA^1#-tkUJ zr=AcjCS&>Cl_m0^tbt9=j zwHcR!7Ba^rNOAGKxGr)02VA5~4%w*nNpb&}4UA%m%?^3UHj?g8GlhpT?|xjq4hTV2@kKvQ-3WmfEk30t1ARCiO7l=xC8 zJ;6#q0UQ^VX-6L9YS~_0qNwfNcsvNSKHJm30eGE@*!({WDgaz=#6co@D)n?y7bDMk zfBwvAqy@KNHgx+1s=An=#4J%09oCD?oUX6PRMM;`=XsFNmXc+BeS88c*%G}if1B-6 z-?iMKjbn@i^_QYipMXp~LNDD~K{y*IicnDpH~cP{t&ma}fnrtZj(>^QIqSke(L?HI zL_x?>vmWQMy&4Bg@}HZ6ib9L=MRgc*rHh>~psrx5NtbHo+%=}5NjZYTb&6nzh8C)d zSV8Px5^@D4sDLg2o&qQYYK@aUfQYYbno{yzpDa@n)D~I$$iojU+cdP6JK1p-Ky4vP zjG+Y-g%t=D1OCmYnlX>L86!iHN9A~NEK%Gnx)D5rPeRXjzLMh>UlBw17TN`|xEOZ% zIK%RCVqz*bem=sATzVjjdPH#3!E_}?W;BH~|G5IUJ$u{EgixWe#yUfwQ^w?c>ITeC5@~W#VS)^J z>2dyuU~ckVAC*dQ_3HPLFSh2PBgb5Dv*PcMmf%LMcJ39i@XlL;9KimKNAi&M+L`jg zqJ-7jjb!fKc?s^lu0&mZBnVZYeFkU)NzOx>Tu|EkTgbr237_M7aoEKm0ODvfNwJ;|UNe?J>q1wK)iPMru z9uQir1mGm(-^?N)mAdz?P;Ona7)EA#t{u9>f_4I|@Z-V?BuzmI=sIM5i+KqfI>pph z72L>4r|vqFsaM?7)3GT&X5Wcc;kXCxzaWa!Uec7GLKq##lmsveN-%gTeIY7mPY z8x6Zorv$>$x<$^;pl0?YmOozT!m!Odm@nk@aDY+Z<<PZ})~tY+QbQu!dp*_00|Z6}$la8q4;>6Y#S~!jmWEg`d4K|Hx#OHM=AiO5>mF&&`x{M!r6t10Bx~EKX?rJo zkbC|fbMO!#f6=21D77>tH>BSDh1Bq0cY0H11eh&f^1<#M zs5E*_8%kbe2Q<=Q0L2?1N}>U_@NAy0J@Whb25Yk0dNzkX+xjre2^E>?Ggj*3G>>vF$34a*6WGYni80d?8$;o4pSKS+^*h(?Xs=pt_g z+v!7Upu#XEfvZbsOXGH(sYLSNRG}CIYaU&I{D;lZ0o5}br~+CGa06n+gG1TbAu@wrln`nRb9TX+5D z503V_eDD5^x=Hm#SH|arUyPm}x`?U=l=#x|mly8>nFD0565}1hEBC1weRlXN)rifX zf(wW1;|ZL@p&B(B5mr8j&j?gi%=19@^N?nxfDqR1osM%Qo!6oWfxn6COlF~GIdmS@ z(0O>5hKSxadbFzOTs3Sq6O>x8hDH>WSH8i0Lt+KTafj&f=3^VszrdjiWeiX@`IVo< z#5t#Bh+I_yBCuZ+*w1qSEo++yl;%vM+Evb3CKAl#vYukcusLKOp-z@_8sNY}Z&Aeg zK0Pwz_-9wq7KeK#ey2r2eH3gx>=-!-quTu6(O?5DED+NDZuZEYm?n;^H2iC5-HwTOg>*hIQ0( zQmO=YK0gp&F#q}0w5u3XJLftJ z9LN#&t^N!KZMWL*_P_zSu;<9e@N=mFcix3c?n{ZvsMM0Ho?N*oaG!3{cTyGEkz9zg z+<^Bqg&V7vmdim6N6u8jDA2{g!07&|j=$736I}4XlH?JM{?F&_%s~1!L-z{9c`SXf zGR}bUXo9ANOwm-Kvv?PQgQS+PcC%uD;*|BUi;tC?GI{O;$hq|CKWdM@FEyiDQ(5>h z=%WEW9yn=I&q&a7!1!*Is8Y=*Utc144X=VqaXyS2tye8Ey(YyjUrmH(F%vXFgm*|R z-;(PW#1vnU4y!Fygsx*Dmya&?kpP7U?b0F>C- zmaDsXl7aKU{$D`_Dqa5ufU?4JhN)Ru0mA4pWhPVEAkgK}&A;ku!RVn4I!RPZ%SBzF zHlXeK5t#W7Nw(r^s3tSw)q$>k2sC4}AD-dp^N*Z@FK%|iOj8HIkJ5OXl=RP@sKpo1 z^=hSdHFTfJO@FrVj#uyr)Ph6@U8wtf?xUbLmCbL-8#}D23#O`keVt@7yX_m|3$u)i z-@kGUM#x7mBCEGfF?OgZotaTcFZgVJ1S5l8Qc&HSyEDE$>6kxRqXhC~Bk$}CGUMk8 z1dK06FQOZ?Nqhy|#W?vRrI52JSTX;G;bJQ8rkH8SfWBP^4U@so2b6b!R5KVy<*j4S z%S(elqgOq!;l3<7?# zq7ljAkkwanetD6FSL!M!l&1Do`eCMhI$#6})-dgh9#5Ph1JRo5>X|hrud(W_RFr(2 z37Ow~kc`UUCzVAzPqbDo9Pc_BvC(EFh6*42*0!<@JI{(zE!{KjinOS*QbGtwbtSkQ zEovPGBN%K448>?^0Mmb(l#xt}v2ikJy3qoqwabzzZV4D+A3e}PGX=G;9qtj+%IV}x z(}W3YZfBC@daPfy7_ZdADV5K@P^b{!hK`J}AN9I345Ao`cbtlaf{4xJ?hZ(jT*nKAhK} zgAC5|oiH`QwIo8f_z<5!7^eoRJ~YA_F2?A7>;Z3d1$Q0L?0>c{`s<#exTU5GZ{wlf z4MkzXL6DAHxR}5Gw+ybQs+SJsp9X2{65(CQ<3zvk&3pL`jVV(j+EHbe=a~9zMah8Y zqhsm$2WQ1|^d&0)cHOOmx>s$6`WS)MJP&E!)Y z)`16>qQ3>X3Pncr$}&R|==HNGVS^)}E-*=uJS`7Nkm;ZdiU5sAF1(FW4)n&i!Qj3Z zEr|DkQujkPYy%xhN_5di8Ab)_1vEOcssV5R9Y-cITraDi- z`xH+uzSVZ0ICFGz1rKE~A?^3^vh#L~cVSh6ZV(M!x2#7)D*B(!t{Q8P^Sq`tUaT5 zUp5%!pMfsOT3?`cwu_F%^6lGVk?iXffV_h5+c`mvQl!S9{m`L|gaf3mnPZenl@(hj4fB6(@rr;4QYmE|;o?Z*Qn5P%2+Dl8Ld1i^QU1ia{n^61e zpT8H~uJJT-I&?h_x!nJXhm2q(3fzT<7bp)lik}S37(OXUpxaDcm=os zn{>*a7!a{^F@?d#ypzymzw{w|i`R@5g&H=$ySu{PjjB4^cVsad>^%q5>i@lH?!9I2 zHU}EB_$o)@x@$53uD(xihaiTU8FXAi^LRk>K>z-)cC7#Rr2lR3f4%PK`N)%o&SjnR za4KZOOnrw7u}kU8#NOY({SCMhS(g9qDh0uuB1If-9jOsYJ(ue9hEe`_77%owI%?<# zMTeEftB#Jc{&<~{$}WChosrFx<9YA;^{Pa3>^APKDOI8Wg!0*^dm^{~{gwYl;y-_Z z&puoHy8k~Oe7cuG?({$3`;>d*f8*CqwHcY1OtI=+3ri_)q50>KzSp4zeMVl@)a;CZ zwMos0cPm#yHhAf+1UFrsxsj9ZvhY~Ey{(F}L&WIlqmU<@A^ne7<`X$rziAa&P1L23 zZ)}_*RD2fkCaO{Pf9)Qf?3*e!r%?4KO6*FG1elGL@j^R0JTQE7G2hHT=mPaF{;vvR z?)1B3F;yhl5(g4)j+2&u>pGD6udbUiF!F_0SCdG;bt8p-I47r$nwKze-}{*zk%@@` zd1+e|*Zr0cU0UDiA+4`@dmb#DqZEX-TQb<1-X1-(zVqY>CuGaFXl7_WV7$g^4s(2{;d;t=1FFC(#>R)N8QmKsdHN>w|7FmQwtYDRQ)TbsSq%`S@N%-GhE7ZA)!Yqnwq>l>Rw{v z;&k97&B=yBvf8d43yzQkw|`{U|HVW^avr7EST{ev-afuSKtz;64fgfTNc{dd?Aqcn zcGYUUA?E7MyWg0s*}i{&Q(+nPwp!v${B4^zw4xnd%mKwuo|eXgX;F~^jT`uGVg7GE zsG9tfrVe90qt~r>c_V7Hh~d0;<3_1XFCHtR>8F3_%r{%)jZR00+k}J}DgiF|lyz!lV=FO4ex3jyKDM6TRJwYQ2 z4nniSjko?F>me!WKc&yE=2?B0NR28pzw^k|mCN=tp<>F@9eGR<&0CK8W_85qlD*#E zIT4IH7J75Oocd;jGkRx->D1^u%4#CTfAA#mNlm@BMzzp&T5P>BhunNWpF4)*t3;X7 z+5Ruz2L2;CCtZn^Q+-eICO$$F84Op(b85xz?rzPdIVN^?b|}bu;@{EG(*4@3ZPH+V z6n)uOFTLh4m4>e4mHVJuv{5H=A@t=#rRik$51$TsZs2?P&CMYP3)Ee`8X7ec8$Nt& zacpbvc;WIBCi2+th5evsmd(_Mm05|CExQrckjJ}>5VF(=-U0u$Axl?BN{q_+73t)@ zh8d8;1_tC{%mST%o@KtozdE!bxUx1K)i*gdWHvz;A(yD~^>hQDQF<&%x_e1gtw`f7 zK$>Qq#-QGff8M#iuxeY4Cwxb-q~BrCq_fWe0#B#W;)TN=kAvgx3lkG6dZrVe>wczR z1d=U}%z3L5Kan<`eiiv&V!ZQHSWaV;GT5aEc^(x+{{d42g0m}&1lqi_^&>i&x|7tea zP$nYvpM;XQK^RSZWaOO&0VfWBgES(NxEBU!f%4n zd?PYDBS0Qu0B5K<*2w@9=vk1OA8@8joC@pR_|qMOcrjIDe-)gi1MZjr%mhBwpQ;NT zT_cnQu1|&=PxLDHxV^Zjp8MkkKJa!ZqrXK%8T)hdt?&d+(%oapck-zst!b~P&-DrY z4gPFycFy%LmkYUHmkQj5l`s927^_{qDa&7Ft$)xbS0QgEreSuge$XeatY(x{=*W!_ zcOn>SKDL8~6{wXw0~sNE#?f1|((t}6pX+O>sXE^&&%wMO?_9U$?)0;}?sGt42OU5R z@&;_Y^Q9@Y62V5z!*h$umW`dzWM{QxB>sgxWA|05+lAUEv*J$sPS1=|fdZA+4 z94e&1o1++}g;dsmqQ8C(jf`=|2Wjg@0a`kdEAY_Gb0ABKNGalASK^buzkYWY?}weU zn~vbXkx1c3NTF107n7uPhy;{Wx(_B?3`uWvOYE9DJoSFuf8ZJP=g-sqoXyS^Cwrb( ziIrWC6(b+aYc?4_D+zrPrmUkQ;&FW02ZtqpNGC7m{*>W^vt{!_u-Kig?fsT_uB8rW zaz@MKdp*KSa?7@N1wWf_PG5y}ttvy_2AIa-TGUsu2%6}#chUWgb6%C?8elc~n(UpE@ZK(03wN7(T z^|aSjC~>RyegxjLO|m_hb|!H2_HOhlx6PLrZwgu(p3fgYmJ<;rWfzyQN!#1sCX^ui zKyc-SoY#rqc$Jj#j~=$cEPI-|VwJIiO#cVOWN&n=@F!ezlyr1j0vn=Wz_Aqz)tE}~ zABPlMHt;#XCS2FVc~hcfP+{vYj!sTq?k!$6OYk=!m5t{iUR;)m8%VUccJrnZfT^F< zW3q`t0i&@j@vn9TMMTImE|BMq?}Rz0|F7i}-Ou{qMPa@#eURKu|G`R}1?hkc}PNNjlM4S#Hsv zw_a3P$%~J0p24`eDThK6xodppZ-DhHiG1{h!FXieeAmPrA7=nS_+uA+fAYnxp*7w+ zZZ)f-74o0wvp?Xrw{im`qoRV%M}NI}D<*!A*jYHC*l~TFv!St(HwHm8HP9=v^$^Uby)sJ`Cs%b%w%8{?wRjo=WS z%w`}i_620n_I6sG8oT?8s%Pf1nc{@$nobYrv@4A}!#MEp`jujArcRW#JOaBxmL&&X z&Un0DJ@djmp<@%IXn(^>Eq9YqeJ9;{bcErx7ovu zqmXs)L5Pl43tw%YnF-gVl1;atmt@3iW+PH5|4Rofs0iExyE=08@_U%4bj zSgnA#^#zYJ2A^1Lnkp>E90p!#U1;%x7*NAYmk0g^r7unHJXWbngOaf|=B<1xYg+{X z{upASl=+TO$OUqxb*%18PO-#Waq+95dglTfPimv&lgFbPY^VcXmFKRm`a<5X*=+C% z3`gtyIOmhvw|HQWe%P7m?VKFBjKdYwZag7n-`uZRW=1BMfQ+)?6@YiffIdXDo#!hq0=OG zb-hzPzqBN7G9Z^&3K(klVh@*>w|8#tLW`KqbVFc$d-5?B_rEeEQhWh73*t+;*+)>8 zRp+Aa3ICU)U%!5BxO49>H4FcXBdf9ONJfZVweU#QFE0mbwae z?*dl;%~=4oqws38)lW709+k{aT_|vxFJNX>%>gI|JJb!tAZOxC7Ck=B;%ygD9Oe^H$u z-YN6&cyRxTzXS8K1VEu*4Vo$d%MGRj?r4@d(RRRnw;$5IvO2WPvXWkGKkMJ=>U8{k z>#k_6-Sz{(kAj2{6-lW+eEr;(O%T(OE`KMdWT?d<3izT|`Mx$F4@ZV8c(Ma>nYqOf z_hEOyrxp**20o@ocJ=__)pTj7UwGnkPy^KvKy?laBH1B2B`}u>AobsTaCE>%8+I?e zI|LrF=!Nwx&dG;85e%CHi1K;XH_&=4DI`ReG1z+SiVEON(M!Di%jCi+PEEqz@ z9aue`B0b&ZmGOMD)9LvpO(^;Gp$aJ?hD2q}tpU?QMDuOAq1@sexlBd;)<3&qJup_* z*?EOWj@56)#q|@wr2Fgre=_}oVK9NtsX^d+A3j`$$;1{L_cerf@kDneRL-z(^t!$f z+&5E?j(^mv2)t#c5Eolk=Fp^3cSXf%2o`;e#iRr3qQ71jO2Ok6A5$>0Wm;QXYXHg{ z{E=du|9?eXa$dt*;84=R`3ZUm2BR(7U*JazHF_#n7qbDcx0?BunAz#@p&b+G3$nvyZztlP3B*v~x-JzwWjahJhtn}c) zoY{0C1N=4tfx-C)(dbycIpFQ~M`Z+wQ*TW4^qkEGX@Ve*lGWxWAYd`bSWHVyTV>Z6%_+E+OWVBygb)c!9Fhe?Bi`i3$`$~B7AKpCUbwq6+K8x9j7Tqyo9ZsVrLJnBSD3 zetRS4?Qt*{{`YLO6_BV$Cfkw3%z9-X>+_sNm;lckL;krZ5e_c;_IQXu?lT~rBv4aO z5K0jaCb>!23HaS{L1p2Y1q-p@iIeRE?as`-^`R&h?P>@yfmvT!?t96y7$pqk8F@y# ztVjM3hz#BN9^wvDD~J(hqRD!e^IcSY0OhJM?Ph%qL_4S+A$4s|IKH@EDW>grQ9QpV zIZJ{>CaM1$P!h5UoO4k7-xVk0G=#PK<7bKOngMsp&WG(Vu1P$ z{Bs%SR;aBG!1xGtXRE@4-D2UM4XXEQ(Mwudx-=k-+#@A@p9j~$&xwzJ2NRms12tkg z-<7LZA>%h-msWqK96bRF$xGlC?NDhvAV3bh`*};N+w2jF?4{R>ZoqYSHx{_fN4RcZ zAXr=<<&p8M4iU(Yo6X#T^d8QgofCId0B0`L*uU-L?5(6nKAd{L_2$Dz%75M-#j1cBHHCy{V-omZV5 zE9H3gcZRr~ki*@V$=X;V@Q1CkzAM@jLfJV&Y@hp-RhUdx*g{U=(8#xosUlObZJOLmFkA zPm^piUuE9xhCIBxMCZHVH+}5h0s^k4)+czN@lAtyk_A+zJV2(Qr6n<&*|-eGNE6td ze7d7R0<88=br)0Z1mI|u7U-6#sg~r)I_En8E_4x8K;ChI78LrJdwXy1kBwcePIMe1 zU9Ct9&_oCjZTg{N<=GbC+W``C8w)9(g_DlqF*BK~e86~A0Ng^|YEbe!C_)nMo2$oA-F3^JKVh}`;;(;ywQc^N*57B(FDrgv V8LWggH9(gQQeyINi$(N5{y#)d+qwV% diff --git a/test/interpreter_functional/snapshots/baseline/combined_test2.json b/test/interpreter_functional/snapshots/baseline/combined_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test2.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/combined_test3.json b/test/interpreter_functional/snapshots/baseline/combined_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/combined_test3.json +++ b/test/interpreter_functional/snapshots/baseline/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/final_output_test.json b/test/interpreter_functional/snapshots/baseline/final_output_test.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/final_output_test.json +++ b/test/interpreter_functional/snapshots/baseline/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_all_data.json b/test/interpreter_functional/snapshots/baseline/metric_all_data.json index 26f111e9edcf92..9b0122c1574812 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json index 7f64f978451915..2d6e756a7f0a3a 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json index e171a65be8bab5..37c6885d76cb08 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/baseline/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json index ed8b0b258fd90f..60a0e450906a27 100644 --- a/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_1.json b/test/interpreter_functional/snapshots/baseline/partial_test_1.json index 8a349aa5df0600..6b2f93b47c0b28 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_1.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_2.json b/test/interpreter_functional/snapshots/baseline/partial_test_2.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_2.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index c1e429508c37ff..4241d6f208bfd4 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test2.json b/test/interpreter_functional/snapshots/baseline/step_output_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test2.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/step_output_test3.json b/test/interpreter_functional/snapshots/baseline/step_output_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/baseline/step_output_test3.json +++ b/test/interpreter_functional/snapshots/baseline/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json index 1325c7fbed03ea..ae1e817424cb10 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json index 2b063b518665af..c0da4794728807 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json index 6152fd406961fc..c5fbcd63b06854 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json index e4c6b09a264ddb..b67b0744494034 100644 --- a/test/interpreter_functional/snapshots/baseline/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/baseline/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test2.json b/test/interpreter_functional/snapshots/session/combined_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/session/combined_test2.json +++ b/test/interpreter_functional/snapshots/session/combined_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/combined_test3.json b/test/interpreter_functional/snapshots/session/combined_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/combined_test3.json +++ b/test/interpreter_functional/snapshots/session/combined_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/final_output_test.json b/test/interpreter_functional/snapshots/session/final_output_test.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/final_output_test.json +++ b/test/interpreter_functional/snapshots/session/final_output_test.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_all_data.json b/test/interpreter_functional/snapshots/session/metric_all_data.json index 26f111e9edcf92..9b0122c1574812 100644 --- a/test/interpreter_functional/snapshots/session/metric_all_data.json +++ b/test/interpreter_functional/snapshots/session/metric_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":2,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json index 7f64f978451915..2d6e756a7f0a3a 100644 --- a/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_multi_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json index e171a65be8bab5..37c6885d76cb08 100644 --- a/test/interpreter_functional/snapshots/session/metric_percentage_mode.json +++ b/test/interpreter_functional/snapshots/session/metric_percentage_mode.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":1000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":true,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json index ed8b0b258fd90f..60a0e450906a27 100644 --- a/test/interpreter_functional/snapshots/session/metric_single_metric_data.json +++ b/test/interpreter_functional/snapshots/session/metric_single_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"},{"id":"col-2-1","name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"metrics":[{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"},{"id":"col-2-1","meta":{"aggConfigParams":{"field":"bytes"},"indexPatternId":"logstash-*","type":"max"},"name":"Max bytes"}],"rows":[{"col-0-2":"200","col-1-1":12891,"col-2-1":19986},{"col-0-2":"404","col-1-1":696,"col-2-1":19881},{"col-0-2":"503","col-1-1":417,"col-2-1":0}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_1.json b/test/interpreter_functional/snapshots/session/partial_test_1.json index 8a349aa5df0600..6b2f93b47c0b28 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_1.json +++ b/test/interpreter_functional/snapshots/session/partial_test_1.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_2.json b/test/interpreter_functional/snapshots/session/partial_test_2.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_2.json +++ b/test/interpreter_functional/snapshots/session/partial_test_2.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json index c1e429508c37ff..4241d6f208bfd4 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test2.json b/test/interpreter_functional/snapshots/session/step_output_test2.json index 98c7844e41f19b..84203617ff8535 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test2.json +++ b/test/interpreter_functional/snapshots/session/step_output_test2.json @@ -1 +1 @@ -{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file +{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/step_output_test3.json b/test/interpreter_functional/snapshots/session/step_output_test3.json index 310377eadd165a..af9fe198d88ea5 100644 --- a/test/interpreter_functional/snapshots/session/step_output_test3.json +++ b/test/interpreter_functional/snapshots/session/step_output_test3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"dimensions":{"bucket":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"metrics":[{"accessor":1,"format":{"id":"number","params":{}},"type":"vis_dimension"}]},"metric":{"colorSchema":"Green to Red","colorsRange":[{"from":0,"to":10000,"type":"range"}],"invertColors":false,"labels":{"show":true},"metricColorMode":"None","percentageMode":false,"style":{"bgColor":false,"bgFill":"#000","fontSize":60,"labelColor":false,"subText":""},"useRanges":false}},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"metric"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json index 1325c7fbed03ea..ae1e817424cb10 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_all_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_all_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json index 2b063b518665af..c0da4794728807 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_fontsize.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":40,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":20,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json index 6152fd406961fc..c5fbcd63b06854 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_metric_data.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"single","scale":"linear","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/tagcloud_options.json b/test/interpreter_functional/snapshots/session/tagcloud_options.json index e4c6b09a264ddb..b67b0744494034 100644 --- a/test/interpreter_functional/snapshots/session/tagcloud_options.json +++ b/test/interpreter_functional/snapshots/session/tagcloud_options.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","name":"response.raw: Descending"},{"id":"col-1-1","name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file +{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":1,"format":{"id":"string","params":{}},"type":"vis_dimension"},"maxFontSize":72,"metric":{"accessor":0,"format":{"id":"string","params":{}},"type":"vis_dimension"},"minFontSize":18,"orientation":"multiple","scale":"log","showLabel":true},"visData":{"columns":[{"id":"col-0-2","meta":{"aggConfigParams":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"indexPatternId":"logstash-*","type":"terms"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"aggConfigParams":{},"indexPatternId":"logstash-*","type":"count"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"kibana_datatable"},"visType":"tagcloud"}} \ No newline at end of file From c76519e15c4ca10c9e2c7fc5b0dcb8216d7661b0 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Mon, 3 Feb 2020 14:02:28 +0000 Subject: [PATCH 39/62] enable darwin for the node installation in the CI (#51705) * enable darwin for the node installation in the CI * refactor: avoid hardcode strings and customise based on the OS flavour * fix classifier Co-authored-by: Elastic Machine --- src/dev/ci_setup/setup_env.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 5217fdf002be9c..823c70e80fe7cd 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -56,23 +56,23 @@ export KIBANA_PKG_BRANCH="$kbnBranch" ### ### download node ### +nodeVersion="$(cat "$dir/.node-version")" +nodeDir="$cacheDir/node/$nodeVersion" +nodeBin="$nodeDir/bin" +classifier="x64.tar.gz" + UNAME=$(uname) OS="linux" if [[ "$UNAME" = *"MINGW64_NT"* ]]; then OS="win" + nodeBin="$HOME/node" + classifier="x64.zip" +elif [[ "$UNAME" == "Darwin" ]]; then + OS="darwin" fi echo " -- Running on OS: $OS" -nodeVersion="$(cat "$dir/.node-version")" -nodeDir="$cacheDir/node/$nodeVersion" - -if [[ "$OS" == "win" ]]; then - nodeBin="$HOME/node" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-win-x64.zip" -else - nodeBin="$nodeDir/bin" - nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-linux-x64.tar.gz" -fi +nodeUrl="https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/dist/v$nodeVersion/node-v$nodeVersion-${OS}-${classifier}" if [[ "$installNode" == "true" ]]; then echo " -- node: version=v${nodeVersion} dir=$nodeDir" From b64f0a76fd4eda536f6fc0cda891a45620249e9c Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Mon, 3 Feb 2020 15:33:57 +0100 Subject: [PATCH 40/62] [ML] Setup apiDocs to generate routes docs (#56006) * [ML] setup typedoc to generate routes docs * [ML] remove typedoc packages * [ML] apiDoc * [ML] update optional params * [ML] address pr comments * [ML] change names * [ML] change description for GetDataFrameAnalyticsMessages * [ML] add custom order --- x-pack/legacy/plugins/ml/.gitignore | 1 + .../legacy/plugins/ml/server/routes/README.md | 16 +++ .../plugins/ml/server/routes/apidoc.json | 21 ++++ .../ml/server/routes/data_frame_analytics.ts | 107 ++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 x-pack/legacy/plugins/ml/.gitignore create mode 100644 x-pack/legacy/plugins/ml/server/routes/README.md create mode 100644 x-pack/legacy/plugins/ml/server/routes/apidoc.json diff --git a/x-pack/legacy/plugins/ml/.gitignore b/x-pack/legacy/plugins/ml/.gitignore new file mode 100644 index 00000000000000..708c5b199467b9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/.gitignore @@ -0,0 +1 @@ +routes_doc diff --git a/x-pack/legacy/plugins/ml/server/routes/README.md b/x-pack/legacy/plugins/ml/server/routes/README.md new file mode 100644 index 00000000000000..1d08335af3d2e9 --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/README.md @@ -0,0 +1,16 @@ +# ML Kibana API routes + +This folder contains ML API routes in Kibana. + +Each route handler requires [apiDoc](https://github.com/apidoc/apidoc) annotations in order +to generate documentation. +The [apidoc-markdown](https://github.com/rigwild/apidoc-markdown) package is also required in order to generate the markdown. + +For now the process is pretty manual. You need to make sure the packages mentioned above are installed globally +to execute the following command from the directory in which this README file is located. +``` +apidoc -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md +``` + +It will create a new directory `routes_doc` (next to the `routes` folder) which contains the documentation in HTML format +as well as `ML_API.md` file. \ No newline at end of file diff --git a/x-pack/legacy/plugins/ml/server/routes/apidoc.json b/x-pack/legacy/plugins/ml/server/routes/apidoc.json new file mode 100644 index 00000000000000..8292e946cd344d --- /dev/null +++ b/x-pack/legacy/plugins/ml/server/routes/apidoc.json @@ -0,0 +1,21 @@ +{ + "name": "ml_kibana_api", + "version": "0.1.0", + "description": "ML Kibana API", + "title": "ML Kibana API", + "url" : "/api/ml/", + "order": [ + "DataFrameAnalytics", + "GetDataFrameAnalytics", + "GetDataFrameAnalyticsById", + "GetDataFrameAnalyticsStats", + "GetDataFrameAnalyticsStatsById", + "UpdateDataFrameAnalytics", + "EvaluateDataFrameAnalytics", + "ExplainDataFrameAnalytics", + "DeleteDataFrameAnalytics", + "StartDataFrameAnalyticsJob", + "StopsDataFrameAnalyticsJob", + "GetDataFrameAnalyticsMessages" + ] +} diff --git a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts index 7b855e5f87cbf6..67fa2fba46f1a9 100644 --- a/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts +++ b/x-pack/legacy/plugins/ml/server/routes/data_frame_analytics.ts @@ -15,7 +15,20 @@ import { dataAnalyticsExplainSchema, } from '../new_platform/data_analytics_schema'; +/** + * Routes for the data frame analytics + */ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteInitialization) { + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics Get analytics data + * @apiName GetDataFrameAnalytics + * @apiDescription Returns the list of data frame analytics jobs. + * + * @apiSuccess {Number} count + * @apiSuccess {Object[]} data_frame_analytics + */ router.get( { path: '/api/ml/data_frame/analytics', @@ -35,6 +48,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId Get analytics data by id + * @apiName GetDataFrameAnalyticsById + * @apiDescription Returns the data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -57,6 +79,13 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/_stats Get analytics stats + * @apiName GetDataFrameAnalyticsStats + * @apiDescription Returns data frame analytics jobs statistics. + */ router.get( { path: '/api/ml/data_frame/analytics/_stats', @@ -76,6 +105,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId/_stats Get stats for requested analytics job + * @apiName GetDataFrameAnalyticsStatsById + * @apiDescription Returns data frame analytics job statistics. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stats', @@ -101,6 +139,16 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {put} /api/ml/data_frame/analytics/:analyticsId Instantiate a data frame analytics job + * @apiName UpdateDataFrameAnalytics + * @apiDescription This API creates a data frame analytics job that performs an analysis + * on the source index and stores the outcome in a destination index. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.put( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -130,6 +178,13 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/_evaluate Evaluate the data frame analytics for an annotated index + * @apiName EvaluateDataFrameAnalytics + * @apiDescription Evaluates the data frame analytics for an annotated index. + */ router.post( { path: '/api/ml/data_frame/_evaluate', @@ -154,6 +209,22 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/_explain Explain a data frame analytics config + * @apiName ExplainDataFrameAnalytics + * @apiDescription This API provides explanations for a data frame analytics config + * that either exists already or one that has not been created yet. + * + * @apiParam {String} [description] + * @apiParam {Object} [dest] + * @apiParam {Object} source + * @apiParam {String} source.index + * @apiParam {Object} analysis + * @apiParam {Object} [analyzed_fields] + * @apiParam {String} [model_memory_limit] + */ router.post( { path: '/api/ml/data_frame/analytics/_explain', @@ -178,6 +249,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {delete} /api/ml/data_frame/analytics/:analyticsId Delete specified analytics job + * @apiName DeleteDataFrameAnalytics + * @apiDescription Deletes specified data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.delete( { path: '/api/ml/data_frame/analytics/{analyticsId}', @@ -205,6 +285,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_start Start specified analytics job + * @apiName StartDataFrameAnalyticsJob + * @apiDescription Starts a data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_start', @@ -229,6 +318,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {post} /api/ml/data_frame/analytics/:analyticsId/_stop Stop specified analytics job + * @apiName StopsDataFrameAnalyticsJob + * @apiDescription Stops a data frame analytics job. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.post( { path: '/api/ml/data_frame/analytics/{analyticsId}/_stop', @@ -263,6 +361,15 @@ export function dataFrameAnalyticsRoutes({ xpackMainPlugin, router }: RouteIniti }) ); + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/:analyticsId/messages Get analytics job messages + * @apiName GetDataFrameAnalyticsMessages + * @apiDescription Returns the list of audit messages for data frame analytics jobs. + * + * @apiParam {String} analyticsId Analytics ID. + */ router.get( { path: '/api/ml/data_frame/analytics/{analyticsId}/messages', From 7dc8c6f9ba15d4566147dbac144b32becdf2adee Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 3 Feb 2020 07:51:53 -0700 Subject: [PATCH 41/62] [Uptime] Fix flaky functional test for #54541 (#56449) * Add timeout block to race-prone functional test code. * Add timeout for pagination click functions. Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/uptime/overview.ts | 12 ++++++++---- x-pack/test/functional/services/uptime.ts | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 73b91a61196bfa..9a879032fadc14 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -53,8 +53,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('pagination is cleared when filter criteria changes', async () => { await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await pageObjects.uptime.changePage('next'); - // there should now be pagination data in the URL - await pageObjects.uptime.pageUrlContains('pagination'); await pageObjects.uptime.pageHasExpectedIds([ '0010-down', '0011-up', @@ -67,9 +65,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '0018-up', '0019-up', ]); + await retry.tryForTime(12000, async () => { + // there should now be pagination data in the URL + await pageObjects.uptime.pageUrlContains('pagination'); + }); await pageObjects.uptime.setStatusFilter('up'); - // ensure that pagination is removed from the URL - await pageObjects.uptime.pageUrlContains('pagination', false); await pageObjects.uptime.pageHasExpectedIds([ '0000-intermittent', '0001-up', @@ -82,6 +82,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '0008-up', '0009-up', ]); + await retry.tryForTime(12000, async () => { + // ensure that pagination is removed from the URL + await pageObjects.uptime.pageUrlContains('pagination', false); + }); }); describe('snapshot counts', () => { diff --git a/x-pack/test/functional/services/uptime.ts b/x-pack/test/functional/services/uptime.ts index ed39f28aabbfad..1d8e0c97b99c4d 100644 --- a/x-pack/test/functional/services/uptime.ts +++ b/x-pack/test/functional/services/uptime.ts @@ -38,10 +38,10 @@ export function UptimeProvider({ getService }: FtrProviderContext) { await browser.pressKeys(browser.keys.ENTER); }, async goToNextPage() { - await testSubjects.click('xpack.uptime.monitorList.nextButton'); + await testSubjects.click('xpack.uptime.monitorList.nextButton', 5000); }, async goToPreviousPage() { - await testSubjects.click('xpack.uptime.monitorList.prevButton'); + await testSubjects.click('xpack.uptime.monitorList.prevButton', 5000); }, async setStatusFilterUp() { await testSubjects.click('xpack.uptime.filterBar.filterStatusUp'); From 1c849acdd57859256d94080682ea933680c9cef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Mon, 3 Feb 2020 16:00:17 +0100 Subject: [PATCH 42/62] [Logs UI] Add smoke tests for log rate and categories tabs (#55088) This adds function smoke tests for the new log rate and categories tabs in the Logs UI. --- x-pack/test/functional/apps/infra/index.ts | 2 + .../apps/infra/log_entry_categories_tab.ts | 28 +++++++++ .../apps/infra/log_entry_rate_tab.ts | 28 +++++++++ .../apps/infra/logs_source_configuration.ts | 62 ++++++++++++------- .../page_objects/infra_logs_page.ts | 10 +-- x-pack/test/functional/services/index.ts | 4 +- .../test/functional/services/logs_ui/index.ts | 18 ++++++ .../services/logs_ui/log_entry_categories.ts | 23 +++++++ .../services/logs_ui/log_entry_rate.ts | 23 +++++++ .../log_stream.ts} | 17 +++-- 10 files changed, 182 insertions(+), 33 deletions(-) create mode 100644 x-pack/test/functional/apps/infra/log_entry_categories_tab.ts create mode 100644 x-pack/test/functional/apps/infra/log_entry_rate_tab.ts create mode 100644 x-pack/test/functional/services/logs_ui/index.ts create mode 100644 x-pack/test/functional/services/logs_ui/log_entry_categories.ts create mode 100644 x-pack/test/functional/services/logs_ui/log_entry_rate.ts rename x-pack/test/functional/services/{infra_log_stream.ts => logs_ui/log_stream.ts} (70%) diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index b706dc8cce5469..597b522a88c51b 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -12,6 +12,8 @@ export default ({ loadTestFile }: FtrProviderContext) => { loadTestFile(require.resolve('./home_page')); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./log_entry_categories_tab')); + loadTestFile(require.resolve('./log_entry_rate_tab')); loadTestFile(require.resolve('./logs_source_configuration')); loadTestFile(require.resolve('./metrics_source_configuration')); loadTestFile(require.resolve('./link_to')); diff --git a/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts new file mode 100644 index 00000000000000..c703738e37228a --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts @@ -0,0 +1,28 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const logsUi = getService('logsUi'); + const retry = getService('retry'); + + describe('Log Entry Categories Tab', function() { + this.tags('smoke'); + + describe('with a trial license', () => { + it('is visible', async () => { + await logsUi.logEntryCategoriesPage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryCategoriesPage.getSetupScreen()).to.be.ok(); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts new file mode 100644 index 00000000000000..95228a520aaa25 --- /dev/null +++ b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts @@ -0,0 +1,28 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const logsUi = getService('logsUi'); + const retry = getService('retry'); + + describe('Log Entry Rate Tab', function() { + this.tags('smoke'); + + describe('with a trial license', () => { + it('is visible', async () => { + await logsUi.logEntryRatePage.navigateTo(); + + await retry.try(async () => { + expect(await logsUi.logEntryRatePage.getSetupScreen()).to.be.ok(); + }); + }); + }); + }); +}; diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index 183acf3a980eeb..ecad5a40ec42e9 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -10,12 +10,14 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); - const infraLogStream = getService('infraLogStream'); + const logsUi = getService('logsUi'); const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); const pageObjects = getPageObjects(['common', 'infraLogs']); + const retry = getService('retry'); describe('Logs Source Configuration', function() { this.tags('smoke'); + before(async () => { await esArchiver.load('empty_kibana'); }); @@ -32,8 +34,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the log indices to a pattern that matches nothing', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); const nameInput = await infraSourceConfigurationForm.getNameInput(); await nameInput.clearValueWithKeyboard({ charByChar: true }); @@ -47,13 +52,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the no indices screen when no indices match the pattern', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - await pageObjects.infraLogs.getNoLogsIndicesPrompt(); + await logsUi.logStreamPage.navigateTo(); + + await retry.try(async () => { + await logsUi.logStreamPage.getNoLogsIndicesPrompt(); + }); }); it('can change the log indices back to a pattern that matches something', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); const logIndicesInput = await infraSourceConfigurationForm.getLogIndicesInput(); await logIndicesInput.clearValueWithKeyboard({ charByChar: true }); @@ -63,16 +74,19 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the default log columns with their headers', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); + await logsUi.logStreamPage.navigateTo(); - expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'event.dataset', 'Message']); + await retry.try(async () => { + const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - const logStreamEntries = await infraLogStream.getStreamEntries(); + expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'event.dataset', 'Message']); + }); + + const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); expect(logStreamEntries.length).to.be.greaterThan(0); const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry( + const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( firstLogStreamEntry ); @@ -80,32 +94,34 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('can change the log columns', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/settings'); - await infraSourceConfigurationForm.getForm(); + await pageObjects.infraLogs.navigateToTab('settings'); + + await retry.try(async () => { + await infraSourceConfigurationForm.getForm(); + }); await infraSourceConfigurationForm.removeAllLogColumns(); await infraSourceConfigurationForm.addTimestampLogColumn(); await infraSourceConfigurationForm.addFieldLogColumn('host.name'); - // await infraSourceConfigurationForm.moveLogColumn(0, 1); - await infraSourceConfigurationForm.saveConfiguration(); }); it('renders the changed log columns with their headers', async () => { - await pageObjects.common.navigateToActualUrl('infraLogs', 'logs/stream'); - const columnHeaderLabels = await infraLogStream.getColumnHeaderLabels(); + await logsUi.logStreamPage.navigateTo(); + + await retry.try(async () => { + const columnHeaderLabels = await logsUi.logStreamPage.getColumnHeaderLabels(); - // TODO: make test more robust - // expect(columnHeaderLabels).to.eql(['host.name', 'Timestamp']); - expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'host.name']); + expect(columnHeaderLabels).to.eql(['Oct 17, 2018', 'host.name']); + }); - const logStreamEntries = await infraLogStream.getStreamEntries(); + const logStreamEntries = await logsUi.logStreamPage.getStreamEntries(); expect(logStreamEntries.length).to.be.greaterThan(0); const firstLogStreamEntry = logStreamEntries[0]; - const logStreamEntryColumns = await infraLogStream.getLogColumnsOfStreamEntry( + const logStreamEntryColumns = await logsUi.logStreamPage.getLogColumnsOfStreamEntry( firstLogStreamEntry ); diff --git a/x-pack/test/functional/page_objects/infra_logs_page.ts b/x-pack/test/functional/page_objects/infra_logs_page.ts index 6eb1349210bae3..1c58f8dc41eba0 100644 --- a/x-pack/test/functional/page_objects/infra_logs_page.ts +++ b/x-pack/test/functional/page_objects/infra_logs_page.ts @@ -18,12 +18,14 @@ export function InfraLogsPageProvider({ getPageObjects, getService }: FtrProvide await pageObjects.common.navigateToApp('infraLogs'); }, - async getLogStream() { - return await testSubjects.find('logStream'); + async navigateToTab(logsUiTab: LogsUiTab) { + await pageObjects.common.navigateToActualUrl('infraLogs', `/logs/${logsUiTab}`); }, - async getNoLogsIndicesPrompt() { - return await testSubjects.find('noLogsIndicesPrompt'); + async getLogStream() { + return await testSubjects.find('logStream'); }, }; } + +type LogsUiTab = 'log-categories' | 'log-rate' | 'settings' | 'stream'; diff --git a/x-pack/test/functional/services/index.ts b/x-pack/test/functional/services/index.ts index 84d5a792ae6cae..aec91ba9e90341 100644 --- a/x-pack/test/functional/services/index.ts +++ b/x-pack/test/functional/services/index.ts @@ -46,7 +46,7 @@ import { GrokDebuggerProvider } from './grok_debugger'; import { UserMenuProvider } from './user_menu'; import { UptimeProvider } from './uptime'; import { InfraSourceConfigurationFormProvider } from './infra_source_configuration_form'; -import { InfraLogStreamProvider } from './infra_log_stream'; +import { LogsUiProvider } from './logs_ui'; import { MachineLearningProvider } from './ml'; import { TransformProvider } from './transform'; @@ -88,7 +88,7 @@ export const services = { userMenu: UserMenuProvider, uptime: UptimeProvider, infraSourceConfigurationForm: InfraSourceConfigurationFormProvider, - infraLogStream: InfraLogStreamProvider, + logsUi: LogsUiProvider, ml: MachineLearningProvider, transform: TransformProvider, }; diff --git a/x-pack/test/functional/services/logs_ui/index.ts b/x-pack/test/functional/services/logs_ui/index.ts new file mode 100644 index 00000000000000..c70a8470aafce3 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { FtrProviderContext } from '../../ftr_provider_context'; +import { LogEntryCategoriesPageProvider } from './log_entry_categories'; +import { LogEntryRatePageProvider } from './log_entry_rate'; +import { LogStreamPageProvider } from './log_stream'; + +export function LogsUiProvider(context: FtrProviderContext) { + return { + logEntryCategoriesPage: LogEntryCategoriesPageProvider(context), + logEntryRatePage: LogEntryRatePageProvider(context), + logStreamPage: LogStreamPageProvider(context), + }; +} diff --git a/x-pack/test/functional/services/logs_ui/log_entry_categories.ts b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts new file mode 100644 index 00000000000000..b9a400b1556797 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/log_entry_categories.ts @@ -0,0 +1,23 @@ +/* + * 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 { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogEntryCategoriesPageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); + const testSubjects = getService('testSubjects'); + + return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('log-categories'); + }, + + async getSetupScreen(): Promise { + return await testSubjects.find('logEntryCategoriesSetupPage'); + }, + }; +} diff --git a/x-pack/test/functional/services/logs_ui/log_entry_rate.ts b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts new file mode 100644 index 00000000000000..96c69e85aa0a48 --- /dev/null +++ b/x-pack/test/functional/services/logs_ui/log_entry_rate.ts @@ -0,0 +1,23 @@ +/* + * 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 { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogEntryRatePageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); + const testSubjects = getService('testSubjects'); + + return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('log-rate'); + }, + + async getSetupScreen(): Promise { + return await testSubjects.find('logEntryRateSetupPage'); + }, + }; +} diff --git a/x-pack/test/functional/services/infra_log_stream.ts b/x-pack/test/functional/services/logs_ui/log_stream.ts similarity index 70% rename from x-pack/test/functional/services/infra_log_stream.ts rename to x-pack/test/functional/services/logs_ui/log_stream.ts index af113d3afffb42..ce37d2d5a60daa 100644 --- a/x-pack/test/functional/services/infra_log_stream.ts +++ b/x-pack/test/functional/services/logs_ui/log_stream.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../ftr_provider_context'; -import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper'; -export function InfraLogStreamProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); +export function LogStreamPageProvider({ getPageObjects, getService }: FtrProviderContext) { + const pageObjects = getPageObjects(['infraLogs']); const retry = getService('retry'); + const testSubjects = getService('testSubjects'); return { + async navigateTo() { + pageObjects.infraLogs.navigateToTab('stream'); + }, + async getColumnHeaderLabels(): Promise { const columnHeaderElements: WebElementWrapper[] = await testSubjects.findAll( '~logColumnHeader' @@ -35,5 +40,9 @@ export function InfraLogStreamProvider({ getService }: FtrProviderContext) { ): Promise { return await testSubjects.findAllDescendant('~logColumn', entryElement); }, + + async getNoLogsIndicesPrompt() { + return await testSubjects.find('noLogsIndicesPrompt'); + }, }; } From 85988386b6fcc5f71c0b01fd3b6fd14767bdb93d Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 3 Feb 2020 07:04:23 -0800 Subject: [PATCH 43/62] Containers (#56571) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 move state containers to /common folder * refactor: 💡 remove RecursiveReadonly type in state containers * fix: 🐛 assume we are in production by default on server-side Co-authored-by: Elastic Machine --- src/plugins/kibana_utils/common/index.ts | 1 + .../create_state_container.test.ts | 0 .../create_state_container.ts | 25 +++++++++++-------- ...ate_state_container_react_helpers.test.tsx | 0 .../create_state_container_react_helpers.ts | 0 .../state_containers/index.ts | 0 .../state_containers/types.ts | 18 ++++++------- .../demos/state_containers/counter.ts | 2 +- .../demos/state_containers/todomvc.ts | 2 +- .../kibana_utils/demos/state_sync/url.ts | 2 +- src/plugins/kibana_utils/public/index.ts | 2 +- .../public/state_sync/state_sync.test.ts | 2 +- .../public/state_sync/state_sync.ts | 2 +- .../kibana_utils/public/state_sync/types.ts | 2 +- 14 files changed, 31 insertions(+), 27 deletions(-) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container.test.ts (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container.ts (86%) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container_react_helpers.test.tsx (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/create_state_container_react_helpers.ts (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/index.ts (100%) rename src/plugins/kibana_utils/{public => common}/state_containers/types.ts (88%) diff --git a/src/plugins/kibana_utils/common/index.ts b/src/plugins/kibana_utils/common/index.ts index bfb45b88964d89..d4aeb2c0fe4ad4 100644 --- a/src/plugins/kibana_utils/common/index.ts +++ b/src/plugins/kibana_utils/common/index.ts @@ -19,4 +19,5 @@ export * from './defer'; export * from './of'; +export * from './state_containers'; export { distinctUntilChangedWithInitialValue } from './distinct_until_changed_with_initial_value'; diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.test.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container.test.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container.test.ts diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts similarity index 86% rename from src/plugins/kibana_utils/public/state_containers/create_state_container.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container.ts index d420aec30f068c..78bfc0c3e90906 100644 --- a/src/plugins/kibana_utils/public/state_containers/create_state_container.ts +++ b/src/plugins/kibana_utils/common/state_containers/create_state_container.ts @@ -19,7 +19,6 @@ import { BehaviorSubject } from 'rxjs'; import { skip } from 'rxjs/operators'; -import { RecursiveReadonly } from '@kbn/utility-types'; import deepFreeze from 'deep-freeze-strict'; import { PureTransitionsToTransitions, @@ -32,14 +31,18 @@ import { const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable'; const $$setActionType = '@@SET'; -const freeze: (value: T) => RecursiveReadonly = - process.env.NODE_ENV !== 'production' - ? (value: T): RecursiveReadonly => { - const isFreezable = value !== null && typeof value === 'object'; - if (isFreezable) return deepFreeze(value) as RecursiveReadonly; - return value as RecursiveReadonly; - } - : (value: T) => value as RecursiveReadonly; +const isProduction = + typeof window === 'object' + ? process.env.NODE_ENV === 'production' + : !process.env.NODE_ENV || process.env.NODE_ENV === 'production'; + +const freeze: (value: T) => T = isProduction + ? (value: T) => value as T + : (value: T): T => { + const isFreezable = value !== null && typeof value === 'object'; + if (isFreezable) return deepFreeze(value) as T; + return value as T; + }; export function createStateContainer( defaultState: State @@ -66,7 +69,7 @@ export function createStateContainer< pureTransitions: PureTransitions = {} as PureTransitions, pureSelectors: PureSelectors = {} as PureSelectors ): ReduxLikeStateContainer { - const data$ = new BehaviorSubject>(freeze(defaultState)); + const data$ = new BehaviorSubject(freeze(defaultState)); const state$ = data$.pipe(skip(1)); const get = () => data$.getValue(); const container: ReduxLikeStateContainer = { @@ -101,7 +104,7 @@ export function createStateContainer< ), addMiddleware: middleware => (container.dispatch = middleware(container as any)(container.dispatch)), - subscribe: (listener: (state: RecursiveReadonly) => void) => { + subscribe: (listener: (state: State) => void) => { const subscription = state$.subscribe(listener); return () => subscription.unsubscribe(); }, diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.test.tsx rename to src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.test.tsx diff --git a/src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts b/src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/create_state_container_react_helpers.ts rename to src/plugins/kibana_utils/common/state_containers/create_state_container_react_helpers.ts diff --git a/src/plugins/kibana_utils/public/state_containers/index.ts b/src/plugins/kibana_utils/common/state_containers/index.ts similarity index 100% rename from src/plugins/kibana_utils/public/state_containers/index.ts rename to src/plugins/kibana_utils/common/state_containers/index.ts diff --git a/src/plugins/kibana_utils/public/state_containers/types.ts b/src/plugins/kibana_utils/common/state_containers/types.ts similarity index 88% rename from src/plugins/kibana_utils/public/state_containers/types.ts rename to src/plugins/kibana_utils/common/state_containers/types.ts index 5f27a3d2c1dcad..26a29bc470e8ac 100644 --- a/src/plugins/kibana_utils/public/state_containers/types.ts +++ b/src/plugins/kibana_utils/common/state_containers/types.ts @@ -18,7 +18,7 @@ */ import { Observable } from 'rxjs'; -import { Ensure, RecursiveReadonly } from '@kbn/utility-types'; +import { Ensure } from '@kbn/utility-types'; export type BaseState = object; export interface TransitionDescription { @@ -27,7 +27,7 @@ export interface TransitionDescription = (...args: Args) => State; export type PureTransition = ( - state: RecursiveReadonly + state: State ) => Transition; export type EnsurePureTransition = Ensure>; export type PureTransitionToTransition> = ReturnType; @@ -36,9 +36,9 @@ export type PureTransitionsToTransitions = { }; export interface BaseStateContainer { - get: () => RecursiveReadonly; + get: () => State; set: (state: State) => void; - state$: Observable>; + state$: Observable; } export interface StateContainer< @@ -55,12 +55,12 @@ export interface ReduxLikeStateContainer< PureTransitions extends object = {}, PureSelectors extends object = {} > extends StateContainer { - getState: () => RecursiveReadonly; - reducer: Reducer>; - replaceReducer: (nextReducer: Reducer>) => void; + getState: () => State; + reducer: Reducer; + replaceReducer: (nextReducer: Reducer) => void; dispatch: (action: TransitionDescription) => void; - addMiddleware: (middleware: Middleware>) => void; - subscribe: (listener: (state: RecursiveReadonly) => void) => () => void; + addMiddleware: (middleware: Middleware) => void; + subscribe: (listener: (state: State) => void) => () => void; } export type Dispatch = (action: T) => void; diff --git a/src/plugins/kibana_utils/demos/state_containers/counter.ts b/src/plugins/kibana_utils/demos/state_containers/counter.ts index 4ddf532c1506db..0484a906a60d3b 100644 --- a/src/plugins/kibana_utils/demos/state_containers/counter.ts +++ b/src/plugins/kibana_utils/demos/state_containers/counter.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createStateContainer } from '../../public/state_containers'; +import { createStateContainer } from '../../common/state_containers'; interface State { count: number; diff --git a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts index e807783a56f319..0a07d721479b3b 100644 --- a/src/plugins/kibana_utils/demos/state_containers/todomvc.ts +++ b/src/plugins/kibana_utils/demos/state_containers/todomvc.ts @@ -17,7 +17,7 @@ * under the License. */ -import { createStateContainer, PureTransition } from '../../public/state_containers'; +import { createStateContainer, PureTransition } from '../../common/state_containers'; export interface TodoItem { text: string; diff --git a/src/plugins/kibana_utils/demos/state_sync/url.ts b/src/plugins/kibana_utils/demos/state_sync/url.ts index 2c426cae6733a4..80c016950d2242 100644 --- a/src/plugins/kibana_utils/demos/state_sync/url.ts +++ b/src/plugins/kibana_utils/demos/state_sync/url.ts @@ -18,7 +18,7 @@ */ import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc'; -import { BaseState, BaseStateContainer, createStateContainer } from '../../public/state_containers'; +import { BaseState, BaseStateContainer, createStateContainer } from '../../common/state_containers'; import { createKbnUrlStateStorage, syncState, diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 00c1c95028b4dc..78828ad9c4b2de 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -25,7 +25,7 @@ export * from './field_wildcard'; export * from './parse'; export * from './render_complete'; export * from './resize_checker'; -export * from './state_containers'; +export * from '../common/state_containers'; export * from './storage'; export { hashedItemStore, HashedItemStore } from './storage/hashed_item_store'; export { diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts index 17f41483a0a21e..c55c60f9b0f890 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BaseState, BaseStateContainer, createStateContainer } from '../state_containers'; +import { BaseState, BaseStateContainer, createStateContainer } from '../../common/state_containers'; import { defaultState, pureTransitions, diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync.ts b/src/plugins/kibana_utils/public/state_sync/state_sync.ts index 28d133829e07c4..ed57723f8f2b7b 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync.ts @@ -23,7 +23,7 @@ import defaultComparator from 'fast-deep-equal'; import { IStateSyncConfig } from './types'; import { IStateStorage } from './state_sync_state_storage'; import { distinctUntilChangedWithInitialValue } from '../../common'; -import { BaseState } from '../state_containers'; +import { BaseState } from '../../common/state_containers'; import { applyDiff } from '../state_management/utils/diff_object'; /** diff --git a/src/plugins/kibana_utils/public/state_sync/types.ts b/src/plugins/kibana_utils/public/state_sync/types.ts index 3009c1d161a532..2acb466d92e920 100644 --- a/src/plugins/kibana_utils/public/state_sync/types.ts +++ b/src/plugins/kibana_utils/public/state_sync/types.ts @@ -17,7 +17,7 @@ * under the License. */ -import { BaseState, BaseStateContainer } from '../state_containers/types'; +import { BaseState, BaseStateContainer } from '../../common/state_containers/types'; import { IStateStorage } from './state_sync_state_storage'; export interface INullableBaseStateContainer From c86ee1b6ea001863fde399721febaf1704fd2fc0 Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 3 Feb 2020 07:05:39 -0800 Subject: [PATCH 44/62] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20AppArch=20p?= =?UTF-8?q?lugins=20to=20CODEOWNERS=20(#56397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 🤖 add AppArch plugins to CODEOWNERS And sort the list alphabetically. * chore: 🤖 add @kbn/interpreter to AppArch CODEOWNERS Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2e2b20a46baede..0b0addf117f6fc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -28,15 +28,7 @@ /src/plugins/dev_tools/ @elastic/kibana-app # App Architecture -/src/plugins/data/ @elastic/kibana-app-arch -/src/plugins/embeddable/ @elastic/kibana-app-arch -/src/plugins/expressions/ @elastic/kibana-app-arch -/src/plugins/kibana_react/ @elastic/kibana-app-arch -/src/plugins/kibana_utils/ @elastic/kibana-app-arch -/src/plugins/navigation/ @elastic/kibana-app-arch -/src/plugins/ui_actions/ @elastic/kibana-app-arch -/src/plugins/visualizations/ @elastic/kibana-app-arch -/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch +/packages/kbn-interpreter/ @elastic/kibana-app-arch /src/legacy/core_plugins/data/ @elastic/kibana-app-arch /src/legacy/core_plugins/elasticsearch/lib/create_proxy.js @elastic/kibana-app-arch /src/legacy/core_plugins/embeddable_api/ @elastic/kibana-app-arch @@ -48,6 +40,19 @@ /src/legacy/core_plugins/kibana/server/routes/api/suggestions/ @elastic/kibana-app-arch /src/legacy/core_plugins/visualizations/ @elastic/kibana-app-arch /src/legacy/server/index_patterns/ @elastic/kibana-app-arch +/src/plugins/bfetch/ @elastic/kibana-app-arch +/src/plugins/dashboard_embeddable_container/ @elastic/kibana-app-arch +/src/plugins/data/ @elastic/kibana-app-arch +/src/plugins/embeddable/ @elastic/kibana-app-arch +/src/plugins/expressions/ @elastic/kibana-app-arch +/src/plugins/inspector/ @elastic/kibana-app-arch +/src/plugins/kibana_react/ @elastic/kibana-app-arch +/src/plugins/kibana_utils/ @elastic/kibana-app-arch +/src/plugins/management/ @elastic/kibana-app-arch +/src/plugins/navigation/ @elastic/kibana-app-arch +/src/plugins/ui_actions/ @elastic/kibana-app-arch +/src/plugins/visualizations/ @elastic/kibana-app-arch +/x-pack/plugins/advanced_ui_actions/ @elastic/kibana-app-arch # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui From 95e40e7fa3a7be1b9818c67177f3e8f4927eb9ba Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 3 Feb 2020 16:22:38 +0100 Subject: [PATCH 45/62] [Uptime] Refresh absolute date ranges for Ping Histogram (#56381) * fix abs date mismatch * fixed types * update pr * simplify params Co-authored-by: Elastic Machine --- .../connected/charts/ping_histogram.tsx | 36 +- .../connected/charts/snapshot_container.tsx | 95 +++ .../filter_group/filter_group_container.tsx | 17 +- .../public/components/connected/index.ts | 3 + .../monitor/status_bar_container.tsx | 85 +++ .../monitor/status_details_container.tsx | 64 ++ .../monitor_charts.test.tsx.snap | 2 - .../__tests__/monitor_charts.test.tsx | 2 - .../functional/__tests__/snapshot.test.tsx | 4 +- .../monitor_bar_series.test.tsx.snap | 156 +++-- .../__tests__/monitor_bar_series.test.tsx | 159 ++++- .../functional/charts/monitor_bar_series.tsx | 25 +- .../public/components/functional/index.ts | 2 - .../components/functional/monitor_charts.tsx | 26 +- .../__snapshots__/monitor_list.test.tsx.snap | 554 ++++++++++++++++++ .../monitor_list_pagination.test.tsx.snap | 4 +- .../__tests__/monitor_list.test.tsx | 25 +- .../monitor_list_pagination.test.tsx | 6 +- .../functional/monitor_list/monitor_list.tsx | 20 +- .../monitor_status.bar.test.tsx.snap | 0 .../__test__}/monitor_status.bar.test.tsx | 12 +- .../monitor_status_details/index.ts | 31 +- .../monitor_status_bar/index.ts | 44 +- .../monitor_status_bar/monitor_status_bar.tsx | 26 +- .../monitor_status_details.tsx | 27 +- .../public/components/functional/snapshot.tsx | 107 +--- .../components/functional/status_panel.tsx | 42 +- .../public/hooks/update_kuery_string.ts | 4 +- .../plugins/uptime/public/pages/monitor.tsx | 17 +- .../plugins/uptime/public/pages/overview.tsx | 11 +- 30 files changed, 1148 insertions(+), 458 deletions(-) create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx create mode 100644 x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx rename x-pack/legacy/plugins/uptime/public/components/functional/{__tests__ => monitor_status_details/__test__}/__snapshots__/monitor_status.bar.test.tsx.snap (100%) rename x-pack/legacy/plugins/uptime/public/components/functional/{__tests__ => monitor_status_details/__test__}/monitor_status.bar.test.tsx (79%) diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx index a6607ca81fc183..cbdd921a36e81b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/ping_histogram.tsx @@ -15,34 +15,40 @@ import { getPingHistogram } from '../../../state/actions'; import { selectPingHistogram } from '../../../state/selectors'; import { withResponsiveWrapper, ResponsiveWrapperProps } from '../../higher_order'; import { GetPingHistogramParams, HistogramResult } from '../../../../common/types'; +import { useUrlParams } from '../../../hooks'; -type Props = GetPingHistogramParams & - ResponsiveWrapperProps & - PingHistogramComponentProps & - DispatchProps & { lastRefresh: number }; +type Props = ResponsiveWrapperProps & + Pick & + DispatchProps & { lastRefresh: number; monitorId?: string }; const PingHistogramContainer: React.FC = ({ data, loadData, - statusFilter, - filters, - dateStart, - dateEnd, - absoluteStartDate, - absoluteEndDate, monitorId, lastRefresh, - ...props + height, + loading, }) => { + const [getUrlParams] = useUrlParams(); + const { + absoluteDateRangeStart, + absoluteDateRangeEnd, + dateRangeStart: dateStart, + dateRangeEnd: dateEnd, + statusFilter, + filters, + } = getUrlParams(); + useEffect(() => { loadData({ monitorId, dateStart, dateEnd, statusFilter, filters }); }, [loadData, dateStart, dateEnd, monitorId, filters, statusFilter, lastRefresh]); return ( ); }; @@ -68,7 +74,7 @@ const mapDispatchToProps = (dispatch: any): DispatchProps => ({ export const PingHistogram = connect< StateProps, DispatchProps, - PingHistogramComponentProps, + Pick, AppState >( mapStateToProps, diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx new file mode 100644 index 00000000000000..6d01ebae1e100f --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/charts/snapshot_container.tsx @@ -0,0 +1,95 @@ +/* + * 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, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { useUrlParams } from '../../../hooks'; +import { AppState } from '../../../state'; +import { fetchSnapshotCount } from '../../../state/actions'; +import { SnapshotComponent } from '../../functional/snapshot'; +import { Snapshot as SnapshotType } from '../../../../common/runtime_types'; + +/** + * Props expected from parent components. + */ +interface OwnProps { + /** + * Height is needed, since by default charts takes height of 100% + */ + height?: string; +} + +/** + * Props given by the Redux store based on action input. + */ +interface StoreProps { + count: SnapshotType; + lastRefresh: number; + loading: boolean; +} + +/** + * Contains functions that will dispatch actions used + * for this component's life cycle + */ +interface DispatchProps { + loadSnapshotCount: typeof fetchSnapshotCount; +} + +/** + * Props used to render the Snapshot component. + */ +type Props = OwnProps & StoreProps & DispatchProps; + +export const Container: React.FC = ({ + count, + height, + lastRefresh, + loading, + loadSnapshotCount, +}: Props) => { + const [getUrlParams] = useUrlParams(); + const { dateRangeStart, dateRangeEnd, statusFilter, filters } = getUrlParams(); + + useEffect(() => { + loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); + }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, loadSnapshotCount, statusFilter]); + return ; +}; + +/** + * Provides state to connected component. + * @param state the root app state + */ +const mapStateToProps = ({ + snapshot: { count, loading }, + ui: { lastRefresh }, +}: AppState): StoreProps => ({ + count, + lastRefresh, + loading, +}); + +/** + * Used for fetching snapshot counts. + * @param dispatch redux-provided action dispatcher + */ +const mapDispatchToProps = (dispatch: any) => ({ + loadSnapshotCount: ( + dateRangeStart: string, + dateRangeEnd: string, + filters?: string, + statusFilter?: string + ): DispatchProps => { + return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); + }, +}); + +export const Snapshot = connect( + // @ts-ignore connect is expecting null | undefined for some reason + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx index 2d1c21d1c997da..569c6bb883cbdc 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/connected/filter_group/filter_group_container.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect } from 'react'; +import React, { useContext, useEffect } from 'react'; import { connect } from 'react-redux'; import { useUrlParams } from '../../../hooks'; import { parseFiltersMap } from '../../functional/filter_group/parse_filter_map'; @@ -12,6 +12,7 @@ import { AppState } from '../../../state'; import { fetchOverviewFilters, GetOverviewFiltersPayload } from '../../../state/actions'; import { FilterGroupComponent } from '../../functional/filter_group'; import { OverviewFilters } from '../../../../common/runtime_types/overview_filters'; +import { UptimeRefreshContext } from '../../../contexts'; interface OwnProps { esFilters?: string; @@ -37,8 +38,9 @@ export const Container: React.FC = ({ loadFilterGroup, overviewFilters, }: Props) => { - const [getUrlParams, updateUrl] = useUrlParams(); + const { lastRefresh } = useContext(UptimeRefreshContext); + const [getUrlParams, updateUrl] = useUrlParams(); const { dateRangeStart, dateRangeEnd, statusFilter, filters: urlFilters } = getUrlParams(); useEffect(() => { @@ -53,7 +55,16 @@ export const Container: React.FC = ({ statusFilter, tags: filterSelections.tags ?? [], }); - }, [dateRangeStart, dateRangeEnd, esKuery, esFilters, statusFilter, urlFilters, loadFilterGroup]); + }, [ + lastRefresh, + dateRangeStart, + dateRangeEnd, + esKuery, + esFilters, + statusFilter, + urlFilters, + loadFilterGroup, + ]); // update filters in the URL from filter group const onFilterUpdate = (filtersKuery: string) => { diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts index 5bb0d1ae8468f9..2fd4c762cf45f5 100644 --- a/x-pack/legacy/plugins/uptime/public/components/connected/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/connected/index.ts @@ -5,6 +5,9 @@ */ export { PingHistogram } from './charts/ping_histogram'; +export { Snapshot } from './charts/snapshot_container'; export { KueryBar } from './kuerybar/kuery_bar_container'; export { OverviewPage } from './pages/overview_container'; export { FilterGroup } from './filter_group/filter_group_container'; +export { MonitorStatusDetails } from './monitor/status_details_container'; +export { MonitorStatusBar } from './monitor/status_bar_container'; diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx new file mode 100644 index 00000000000000..db6337732091a9 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_bar_container.tsx @@ -0,0 +1,85 @@ +/* + * 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, { useContext, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { AppState } from '../../../state'; +import { selectMonitorLocations, selectMonitorStatus } from '../../../state/selectors'; +import { MonitorStatusBarComponent } from '../../functional/monitor_status_details/monitor_status_bar'; +import { getMonitorStatus, getSelectedMonitor } from '../../../state/actions'; +import { useUrlParams } from '../../../hooks'; +import { Ping } from '../../../../common/graphql/types'; +import { MonitorLocations } from '../../../../common/runtime_types/monitor'; +import { UptimeRefreshContext } from '../../../contexts'; + +interface StateProps { + monitorStatus: Ping; + monitorLocations: MonitorLocations; +} + +interface DispatchProps { + loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => void; +} + +interface OwnProps { + monitorId: string; +} + +type Props = OwnProps & StateProps & DispatchProps; + +export const Container: React.FC = ({ + loadMonitorStatus, + monitorId, + monitorStatus, + monitorLocations, +}: Props) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorStatus(dateStart, dateEnd, monitorId); + }, [monitorId, dateStart, dateEnd, loadMonitorStatus, lastRefresh]); + + return ( + + ); +}; + +const mapStateToProps = (state: AppState, ownProps: OwnProps) => ({ + monitorStatus: selectMonitorStatus(state), + monitorLocations: selectMonitorLocations(state, ownProps.monitorId), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + loadMonitorStatus: (dateStart: string, dateEnd: string, monitorId: string) => { + dispatch( + getMonitorStatus({ + monitorId, + dateStart, + dateEnd, + }) + ); + dispatch( + getSelectedMonitor({ + monitorId, + }) + ); + }, +}); + +// @ts-ignore TODO: Investigate typescript issues here +export const MonitorStatusBar = connect( + // @ts-ignore TODO: Investigate typescript issues here + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx new file mode 100644 index 00000000000000..6929e3bd64c4d0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/connected/monitor/status_details_container.tsx @@ -0,0 +1,64 @@ +/* + * 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, { useContext, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { useUrlParams } from '../../../hooks'; +import { AppState } from '../../../state'; +import { selectMonitorLocations } from '../../../state/selectors'; +import { fetchMonitorLocations, MonitorLocationsPayload } from '../../../state/actions/monitor'; +import { MonitorStatusDetailsComponent } from '../../functional/monitor_status_details'; +import { MonitorLocations } from '../../../../common/runtime_types'; +import { UptimeRefreshContext } from '../../../contexts'; + +interface OwnProps { + monitorId: string; +} + +interface StoreProps { + monitorLocations: MonitorLocations; +} + +interface DispatchProps { + loadMonitorLocations: typeof fetchMonitorLocations; +} + +type Props = OwnProps & StoreProps & DispatchProps; + +export const Container: React.FC = ({ + loadMonitorLocations, + monitorLocations, + monitorId, +}: Props) => { + const { lastRefresh } = useContext(UptimeRefreshContext); + + const [getUrlParams] = useUrlParams(); + const { dateRangeStart: dateStart, dateRangeEnd: dateEnd } = getUrlParams(); + + useEffect(() => { + loadMonitorLocations({ dateStart, dateEnd, monitorId }); + }, [loadMonitorLocations, monitorId, dateStart, dateEnd, lastRefresh]); + + return ( + + ); +}; +const mapStateToProps = (state: AppState, { monitorId }: OwnProps) => ({ + monitorLocations: selectMonitorLocations(state, monitorId), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + loadMonitorLocations: (params: MonitorLocationsPayload) => { + dispatch(fetchMonitorLocations(params)); + }, +}); + +export const MonitorStatusDetails = connect( + // @ts-ignore TODO: Investigate typescript issues here + mapStateToProps, + mapDispatchToProps +)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap index f6846dfb1164da..9853ed5cadfc9b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_charts.test.tsx.snap @@ -180,8 +180,6 @@ exports[`MonitorCharts component renders the component without errors 1`] = ` }, } } - dateRangeEnd="2011-12-03T10:15:30+01:00" - dateRangeStart="2011-12-03T10:15:30+01:00" loading={false} mean="mean" monitorId="something" diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx index 81c60c8fbeaaa9..331b5c9c0b0965 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_charts.test.tsx @@ -73,8 +73,6 @@ describe('MonitorCharts component', () => { range="range" success="success" monitorId="something" - dateRangeStart="2011-12-03T10:15:30+01:00" - dateRangeEnd="2011-12-03T10:15:30+01:00" /> ) ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx index d645eb21ac7766..214b0394369f7a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/snapshot.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { Snapshot } from '../../../../common/runtime_types'; -import { PresentationalComponent } from '../snapshot'; +import { SnapshotComponent } from '../snapshot'; describe('Snapshot component', () => { const snapshot: Snapshot = { @@ -17,7 +17,7 @@ describe('Snapshot component', () => { }; it('renders without errors', () => { - const wrapper = shallowWithIntl(); + const wrapper = shallowWithIntl(); expect(wrapper).toMatchSnapshot(); }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap index c3b99c9785cbe0..8ca73879cab8c0 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/__snapshots__/monitor_bar_series.test.tsx.snap @@ -1,67 +1,107 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`MonitorBarSeries component renders a series when there are down items 1`] = ` +exports[`MonitorBarSeries component renders if the data series is present 1`] = `
- - - - - +
+
+

+ No data to display +

+
+
+
`; + +exports[`MonitorBarSeries component shallow renders a series when there are down items 1`] = ` + + + +`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx index 3cede0be00ef1c..c3e98134e438d8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/__tests__/monitor_bar_series.test.tsx @@ -5,15 +5,16 @@ */ import React from 'react'; -import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { MonitorBarSeries, MonitorBarSeriesProps } from '../monitor_bar_series'; +import { renderWithRouter } from '../../../../lib'; +import { SummaryHistogramPoint } from '../../../../../common/graphql/types'; describe('MonitorBarSeries component', () => { let props: MonitorBarSeriesProps; + let histogramSeries: SummaryHistogramPoint[]; beforeEach(() => { props = { - absoluteStartDate: 1548697920000, - absoluteEndDate: 1548700920000, dangerColor: 'A danger color', histogramSeries: [ { @@ -33,20 +34,144 @@ describe('MonitorBarSeries component', () => { }, ], }; + histogramSeries = [ + { timestamp: 1580387868000, up: 0, down: 5 }, + { timestamp: 1580387904000, up: 0, down: 20 }, + { + timestamp: 1580387940000, + up: 0, + down: 19, + }, + { + timestamp: 1580387976000, + up: 0, + down: 16, + }, + { + timestamp: 1580388012000, + up: 0, + down: 20, + }, + { + timestamp: 1580388048000, + up: 0, + down: 15, + }, + { + timestamp: 1580388084000, + up: 0, + down: 20, + }, + { + timestamp: 1580388120000, + up: 0, + down: 19, + }, + { + timestamp: 1580388156000, + up: 0, + down: 16, + }, + { + timestamp: 1580388192000, + up: 0, + down: 20, + }, + { + timestamp: 1580388228000, + up: 0, + down: 15, + }, + { + timestamp: 1580388264000, + up: 0, + down: 20, + }, + { + timestamp: 1580388300000, + up: 0, + down: 19, + }, + { + timestamp: 1580388336000, + up: 0, + down: 16, + }, + { + timestamp: 1580388372000, + up: 0, + down: 20, + }, + { + timestamp: 1580388408000, + up: 0, + down: 15, + }, + { + timestamp: 1580388444000, + up: 0, + down: 20, + }, + { + timestamp: 1580388480000, + up: 0, + down: 19, + }, + { + timestamp: 1580388516000, + up: 0, + down: 16, + }, + { + timestamp: 1580388552000, + up: 0, + down: 20, + }, + { + timestamp: 1580388588000, + up: 0, + down: 15, + }, + { + timestamp: 1580388624000, + up: 0, + down: 20, + }, + { + timestamp: 1580388660000, + up: 0, + down: 19, + }, + { + timestamp: 1580388696000, + up: 0, + down: 16, + }, + { + timestamp: 1580388732000, + up: 0, + down: 20, + }, + { + timestamp: 1580388768000, + up: 0, + down: 10, + }, + ]; }); - it('renders a series when there are down items', () => { - const component = shallowWithIntl(); + it('shallow renders a series when there are down items', () => { + const component = shallowWithIntl(renderWithRouter()); expect(component).toMatchSnapshot(); }); - it('renders null when there are no down items', () => { + it('shallow renders null when there are no down items', () => { props.histogramSeries = []; - const component = shallowWithIntl(); + const component = shallowWithIntl(renderWithRouter()); expect(component).toEqual({}); }); - it('renders nothing if the down count has no counts', () => { + it(' shallow renders nothing if the down count has no counts', () => { props.histogramSeries = [ { timestamp: 123, @@ -64,19 +189,21 @@ describe('MonitorBarSeries component', () => { up: 0, }, ]; - const component = shallowWithIntl(); + const component = shallowWithIntl(renderWithRouter()); expect(component).toEqual({}); }); - it('renders nothing if the data series is null', () => { + it('shallow renders nothing if the data series is null', () => { const component = shallowWithIntl( - + renderWithRouter() ); expect(component).toEqual({}); }); + + it('renders if the data series is present', () => { + const component = renderWithIntl( + renderWithRouter() + ); + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx index ce91bf5b1638f4..2338bf02783485 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/charts/monitor_bar_series.tsx @@ -19,16 +19,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiToolTip } from '@elastic/eui'; import { SummaryHistogramPoint } from '../../../../common/graphql/types'; import { getChartDateLabel, seriesHasDownValues } from '../../../lib/helper'; +import { useUrlParams } from '../../../hooks'; export interface MonitorBarSeriesProps { - /** - * The date/time for the start of the timespan. - */ - absoluteStartDate: number; - /** - * The date/time for the end of the timespan. - */ - absoluteEndDate: number; /** * The color to use for the display of down states. */ @@ -44,23 +37,23 @@ export interface MonitorBarSeriesProps { * so we will only render the series component if there are down counts for the selected monitor. * @param props - the values for the monitor this chart visualizes */ -export const MonitorBarSeries = ({ - absoluteStartDate, - absoluteEndDate, - dangerColor, - histogramSeries, -}: MonitorBarSeriesProps) => { +export const MonitorBarSeries = ({ dangerColor, histogramSeries }: MonitorBarSeriesProps) => { + const [getUrlParams] = useUrlParams(); + const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); + const id = 'downSeries'; return seriesHasDownValues(histogramSeries) ? ( " +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
"`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
"`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
"`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
"`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap index 8a8799207ace81..73d7599a603590 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/components/footer/settings/__tests__/__snapshots__/settings.test.tsx.snap @@ -65,7 +65,6 @@ exports[` can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -85,7 +84,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -110,7 +108,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -130,7 +127,6 @@ exports[`<Settings /> can navigate Autoplay Settings 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -219,7 +215,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -239,7 +234,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -264,7 +258,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -284,7 +277,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -317,7 +309,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M10.843 13.069L6.232 8.384a.546.546 0 010-.768l4.61-4.685a.552.552 0 000-.771.53.53 0 00-.759 0l-4.61 4.684a1.65 1.65 0 000 2.312l4.61 4.684a.53.53 0 00.76 0 .552.552 0 000-.771z" fill-rule="nonzero" @@ -366,7 +357,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.293 8L3.146 3.854a.5.5 0 11.708-.708L8 7.293l4.146-4.147a.5.5 0 01.708.708L8.707 8l4.147 4.146a.5.5 0 01-.708.708L8 8.707l-4.146 4.147a.5.5 0 01-.708-.708L7.293 8z" /> @@ -381,7 +371,6 @@ exports[`<Settings /> can navigate Autoplay Settings 2`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fill-rule="evenodd" @@ -561,7 +550,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -581,7 +569,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -606,7 +593,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -626,7 +612,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 1`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -715,7 +700,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M4.608 3.063C4.345 2.895 4 3.089 4 3.418v9.167c0 .329.345.523.608.356l7.2-4.584a.426.426 0 000-.711l-7.2-4.583zm.538-.844l7.2 4.583a1.426 1.426 0 010 2.399l-7.2 4.583C4.21 14.38 3 13.696 3 12.585V3.418C3 2.307 4.21 1.624 5.146 2.22z" /> @@ -735,7 +719,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -760,7 +743,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M0 6h4v4H0V6zm1 1v2h2V7H1zm5-1h4v4H6V6zm1 1v2h2V7H7zm5-1h4v4h-4V6zm1 3h2V7h-2v2z" /> @@ -780,7 +762,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M5.157 13.069l4.611-4.685a.546.546 0 000-.768L5.158 2.93a.552.552 0 010-.771.53.53 0 01.759 0l4.61 4.684c.631.641.63 1.672 0 2.312l-4.61 4.684a.53.53 0 01-.76 0 .552.552 0 010-.771z" fill-rule="nonzero" @@ -813,7 +794,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M10.843 13.069L6.232 8.384a.546.546 0 010-.768l4.61-4.685a.552.552 0 000-.771.53.53 0 00-.759 0l-4.61 4.684a1.65 1.65 0 000 2.312l4.61 4.684a.53.53 0 00.76 0 .552.552 0 000-.771z" fill-rule="nonzero" @@ -871,7 +851,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.293 8L3.146 3.854a.5.5 0 11.708-.708L8 7.293l4.146-4.147a.5.5 0 01.708.708L8.707 8l4.147 4.146a.5.5 0 01-.708.708L8 8.707l-4.146 4.147a.5.5 0 01-.708-.708L7.293 8z" /> @@ -886,7 +865,6 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M6.5 12a.502.502 0 01-.354-.146l-4-4a.502.502 0 01.708-.708L6.5 10.793l6.646-6.647a.502.502 0 01.708.708l-7 7A.502.502 0 016.5 12" fill-rule="evenodd" @@ -927,4 +905,4 @@ exports[`<Settings /> can navigate Toolbar Settings, closes when activated 2`] = </div> `; -exports[`<Settings /> can navigate Toolbar Settings, closes when activated 3`] = `"<div><div><div data-focus-guard=\\"true\\" tabindex=\\"-1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-guard=\\"true\\" tabindex=\\"-1\\" style=\\"width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;\\"></div><div data-focus-lock-disabled=\\"disabled\\"><div class=\\"euiPanel euiPopover__panel euiPopover__panel--top euiPopover__panel-withTitle\\" aria-live=\\"assertive\\" role=\\"dialog\\" aria-modal=\\"true\\" aria-describedby=\\"generated-id\\" style=\\"top: -16px; left: -22px; z-index: 2000;\\"><div class=\\"euiPopover__panelArrow euiPopover__panelArrow--top\\" style=\\"left: 10px; top: 0px;\\"></div><div><div class=\\"euiContextMenu\\" style=\\"height: 0px;\\"><div class=\\"euiContextMenuPanel euiContextMenu__panel euiContextMenuPanel-txOutLeft\\" tabindex=\\"0\\"><div class=\\"euiPopoverTitle\\"><span class=\\"euiContextMenu__itemLayout\\">Settings</span></div><div><div><button class=\\"euiContextMenuItem\\" type=\\"button\\"><span class=\\"euiContextMenu__itemLayout\\"><svg width=\\"16\\" height=\\"16\\" viewBox=\\"0 0 16 16\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--medium euiIcon-isLoaded euiContextMenu__icon\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"><title>Auto Play
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; +exports[` can navigate Toolbar Settings, closes when activated 3`] = `"
Settings
Hide Toolbar
Hide the toolbar when the mouse is not within the Canvas?
"`; diff --git a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 9256bee4e756b5..353dc58e6d4011 100644 --- a/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/legacy/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -296,7 +296,6 @@ exports[`UploadLicense should display a modal when license requires acknowledgem } > @@ -1401,7 +1399,6 @@ exports[`UploadLicense should display an error when ES says license is expired 1 width={16} xmlns="http://www.w3.org/2000/svg" > - <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> @@ -1871,7 +1868,6 @@ exports[`UploadLicense should display an error when ES says license is invalid 1 width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> @@ -2806,7 +2802,6 @@ exports[`UploadLicense should display error when ES returns error 1`] = ` width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M9 10.114l1.85-1.943a.52.52 0 01.77 0c.214.228.214.6 0 .829l-1.95 2.05a1.552 1.552 0 01-2.31 0L5.41 9a.617.617 0 010-.829.52.52 0 01.77 0L8 10.082V1.556C8 1.249 8.224 1 8.5 1s.5.249.5.556v8.558zM4.18 6a.993.993 0 00-.972.804l-1.189 6A.995.995 0 002.991 14h11.018a1 1 0 00.972-1.196l-1.19-6a.993.993 0 00-.97-.804H4.18zM6 5v1h5V5h1.825c.946 0 1.76.673 1.946 1.608l1.19 6A2 2 0 0114.016 15H2.984a1.992 1.992 0 01-1.945-2.392l1.19-6C2.414 5.673 3.229 5 4.174 5H6z" /> diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap index 2dc355513ece24..c62b07a89e7a3e 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/geometry_filter_form.test.js.snap @@ -33,6 +33,7 @@ exports[`should not render relation select when geo field is geo_point 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -110,6 +111,7 @@ exports[`should not show "within" relation when filter geometry is not closed 1` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -214,6 +216,7 @@ exports[`should render error message 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ @@ -294,6 +297,7 @@ exports[`should render relation select when geo field is geo_shape 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} itemClassName="mapGeometryFilter__geoFieldItem" onChange={[Function]} options={ diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap index 9a55e46b40aea3..9d07b9c641e0f9 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap @@ -15,6 +15,7 @@ exports[`HeatmapStyleEditor is rendered 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap index ee76657c8d27a4..4dd77842894c65 100644 --- a/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap @@ -3,25 +3,17 @@ exports[`Select Flow Direction rendering it renders the basic group button for uni-direction and bi-direction 1`] = ` <EuiFilterGroup> <EuiFilterButton - color="text" data-test-subj="uniDirectional" - grow={true} hasActiveFilters={true} - iconSide="right" onClick={[Function]} - type="button" withNext={true} > Unidirectional </EuiFilterButton> <EuiFilterButton - color="text" data-test-subj="biDirectional" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" > Bidirectional </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap index 4c9a27b76060cb..8f40d0203afd43 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap @@ -5,16 +5,12 @@ exports[`GroupsFilterPopover renders correctly against snapshot 1`] = ` anchorPosition="downCenter" button={ <EuiFilterButton - color="text" data-test-subj="groups-filter-popover-button" - grow={true} hasActiveFilters={false} - iconSide="right" iconType="arrowDown" isSelected={false} numActiveFilters={0} onClick={[Function]} - type="button" > Groups </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap index fac91f75978f09..747ac63551b55b 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap @@ -110,25 +110,17 @@ exports[`JobsTableFilters renders correctly against snapshot 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" data-test-subj="show-elastic-jobs-filter-button" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" withNext={true} > Elastic jobs </EuiFilterButton> <EuiFilterButton - color="text" data-test-subj="show-custom-jobs-filter-button" - grow={true} hasActiveFilters={false} - iconSide="right" onClick={[Function]} - type="button" > Custom jobs </EuiFilterButton> diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap index 7e3e099bf0276e..28481e9970a5eb 100644 --- a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap @@ -3,7 +3,6 @@ exports[`Modal all errors rendering it renders the default all errors modal when isShowing is positive 1`] = ` <EuiOverlayMask> <EuiModal - maxWidth={true} onClose={[Function]} > <EuiModalHeader> diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap index 8930dedfa0035d..6e422bc13f06be 100644 --- a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap @@ -5,7 +5,6 @@ exports[`ConfirmDeleteModal renders as expected 1`] = ` <EuiModal className="spcConfirmDeleteModal" initialFocus="input[name=\\"confirmDeleteSpaceInput\\"]" - maxWidth={true} onClose={[MockFunction]} > <EuiModalHeader> diff --git a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx index 6eed58a7842124..3a4861f4fbc9ec 100644 --- a/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx +++ b/x-pack/legacy/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx @@ -9,8 +9,6 @@ import { EuiButton, EuiButtonEmpty, EuiCallOut, - // @ts-ignore - EuiConfirmModal, EuiFieldText, EuiFormRow, EuiModal, @@ -89,7 +87,7 @@ class ConfirmDeleteModalUI extends Component<Props, State> { // This is largely the same as the built-in EuiConfirmModal component, but we needed the ability // to disable the buttons since this could be a long-running operation - const modalProps: EuiModalProps & CommonProps = { + const modalProps: Omit<EuiModalProps, 'children'> & CommonProps = { onClose: onCancel, className: 'spcConfirmDeleteModal', initialFocus: 'input[name="confirmDeleteSpaceInput"]', diff --git a/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap index d7702e2f18d44b..750afcfc44e7e3 100644 --- a/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap +++ b/x-pack/legacy/plugins/spaces/public/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap @@ -3,7 +3,6 @@ exports[`ConfirmAlterActiveSpaceModal renders as expected 1`] = ` <EuiOverlayMask> <EuiConfirmModal - buttonColor="primary" cancelButtonText="Cancel" confirmButtonText="Update space" defaultFocusedButton="confirm" diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap index 2d3351ec1c0d20..da9153f4a6c8d6 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/filter_bar.test.tsx.snap @@ -6,26 +6,18 @@ exports[`FilterBar renders 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={false} - iconSide="right" key="all" numFilters={2} onClick={[Function]} - type="button" > all </EuiFilterButton> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={true} - iconSide="right" key="critical" numFilters={2} onClick={[Function]} - type="button" > critical </EuiFilterButton> diff --git a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap index b36e0c1a2bfdb0..dfc69c57cfff6b 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap +++ b/x-pack/legacy/plugins/upgrade_assistant/public/np_ready/application/components/tabs/checkup/__snapshots__/group_by_bar.test.tsx.snap @@ -6,24 +6,16 @@ exports[`GroupByBar renders 1`] = ` > <EuiFilterGroup> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={true} - iconSide="right" key="message" onClick={[Function]} - type="button" > by issue </EuiFilterButton> <EuiFilterButton - color="text" - grow={true} hasActiveFilters={false} - iconSide="right" key="index" onClick={[Function]} - type="button" > by index </EuiFilterButton> diff --git a/x-pack/package.json b/x-pack/package.json index ad0be351483f61..99e2a32bf3372f 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -176,7 +176,7 @@ "@elastic/apm-rum-react": "^0.3.2", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.6.0", - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.1.0", "@elastic/node-crypto": "^1.0.0", diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index bb5d553b26b364..5399d139371790 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -84,7 +84,7 @@ It allows you to monitor the performance of thousands of applications in real ti '{config.docs.base_url}guide/en/apm/get-started/{config.docs.version}/index.html', }, }), - euiIconType: 'logoAPM', + euiIconType: 'apmApp', artifacts, onPrem: onPremInstructions(indices), elasticCloud: createElasticCloudInstructions(cloud), diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap index 42fd4417e238b1..f8bbfbc8bb33de 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -49,7 +49,6 @@ exports[`APIKeysGridPage renders a callout when API keys are not enabled 1`] = ` width={16} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M7.59 10.059L7.35 5.18h1.3L8.4 10.06h-.81zm.394 1.901a.61.61 0 01-.448-.186.606.606 0 01-.186-.444c0-.174.062-.323.186-.446a.614.614 0 01.448-.184c.169 0 .315.06.44.182.124.122.186.27.186.448a.6.6 0 01-.189.446.607.607 0 01-.437.184zM2 14a1 1 0 01-.878-1.479l6-11a1 1 0 011.756 0l6 11A1 1 0 0114 14H2zm0-1h12L8 2 2 13z" fillRule="evenodd" @@ -189,7 +188,6 @@ exports[`APIKeysGridPage renders permission denied if user does not have require width={32} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M14 32l-.36-.14A21.07 21.07 0 010 12.07V5.44L14 .06l14 5.38v6.63a21.07 21.07 0 01-13.64 19.78L14 32zM2 6.82v5.25a19.08 19.08 0 0012 17.77 19.08 19.08 0 0012-17.77V6.82L14 2.2 2 6.82z" /> diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap index 37db2e118861e8..a2741773f183bf 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap @@ -39,6 +39,7 @@ exports[`<SimplePrivilegeForm> renders without crashing 1`] = ` fullWidth={false} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index e9f2f946e98859..8d10e27df9694e 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -176,6 +176,7 @@ exports[`<PrivilegeSpaceForm> renders without crashing 1`] = ` fullWidth={true} hasDividers={true} isInvalid={false} + isLoading={false} onChange={[Function]} options={ Array [ diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap index 970cbfd03954a4..4789314d9f780b 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap @@ -67,7 +67,6 @@ exports[`<RolesGridPage /> renders permission denied if required 1`] = ` width={32} xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M14 32l-.36-.14A21.07 21.07 0 010 12.07V5.44L14 .06l14 5.38v6.63a21.07 21.07 0 01-13.64 19.78L14 32zM2 6.82v5.25a19.08 19.08 0 0012 17.77 19.08 19.08 0 0012-17.77V6.82L14 2.2 2 6.82z" /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 47e11817ffa5d4..3a6ba45413de60 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1198,8 +1198,6 @@ "kbn.embeddable.inspectorRequestDataTitle": "データ", "kbn.embeddable.inspectorRequestDescription": "このリクエストは Elasticsearch にクエリをかけ、検索データを取得します。", "kbn.embeddable.search.displayName": "検索", - "kbn.home.addData.addDataToKibanaDescription": "これらのソリューションで、データを作成済みのダッシュボードと監視システムへとすぐに変えることができます。", - "kbn.home.addData.addDataToKibanaTitle": "Kibana にデータを追加", "kbn.home.addData.apm.addApmButtonLabel": "APM を追加", "kbn.home.addData.apm.nameDescription": "APM は、集約内から自動的に詳細なパフォーマンスメトリックやエラーを集めます。", "kbn.home.addData.apm.nameTitle": "APM", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 86d9a69dc0900b..f7cbaa7d72158d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1198,8 +1198,6 @@ "kbn.embeddable.inspectorRequestDataTitle": "数据", "kbn.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "kbn.embeddable.search.displayName": "搜索", - "kbn.home.addData.addDataToKibanaDescription": "使用这些解决方案可快速将您的数据转换成预建仪表板和监测系统。", - "kbn.home.addData.addDataToKibanaTitle": "将数据添加到 Kibana", "kbn.home.addData.apm.addApmButtonLabel": "添加 APM", "kbn.home.addData.apm.nameDescription": "APM 自动从您的应用程序内收集深入全面的性能指标和错误。", "kbn.home.addData.apm.nameTitle": "APM", diff --git a/yarn.lock b/yarn.lock index a3acc2ae216c5a..4b56ec6460775b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1953,10 +1953,10 @@ tabbable "^1.1.0" uuid "^3.1.0" -"@elastic/eui@18.2.1": - version "18.2.1" - resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-18.2.1.tgz#6ce6d0bd1d0541052d21f2918305524d71e91678" - integrity sha512-6C5tnWJTlBB++475i0vRoCsnz4JaYznb4zMNFLc+z5GY3vA3/E3AXTjmmBwybEicCCi3h1SnpJxZsgMakiZwRA== +"@elastic/eui@18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@elastic/eui/-/eui-18.3.0.tgz#e21c6246624f694e2ae1c7c1f1a11b612faf260a" + integrity sha512-Rkj1rTtDa6iZMUF7pxYRojku1sLXzTU0FK1D9i0XE3H//exy3VyTV6qUlbdkiKXjO7emrgQqfzKDeXT+ZYztgg== dependencies: "@types/chroma-js" "^1.4.3" "@types/lodash" "^4.14.116" From 4d3803d310f991dd0ee9f1214b7c6bda50498263 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad <frank.hassanabad@elastic.co> Date: Mon, 3 Feb 2020 16:40:29 -0700 Subject: [PATCH 61/62] [SIEM][Detection Engine] Critical blocker, fixes pre-packaged rule miscounts ## Summary * Found multiple issues with how unstable finds can occur where iterating over multiple pages of find API with saved objects might return the same results per page and omit things as you try to figure out which pre-packaged rules are installed and which ones are not. * This makes a distinct trade off of doing more JSON.parse() on the event loop by querying all the pre-packaged rules at one time. This however gives a stable and accurate count * Fixed the tags aggregation to do the same thing. * Fixes https://github.com/elastic/siem-team/issues/506 ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../get_existing_prepackaged_rules.test.ts | 191 ++++-------------- .../rules/get_existing_prepackaged_rules.ts | 55 ++--- .../lib/detection_engine/tags/read_tags.ts | 46 ++--- 3 files changed, 73 insertions(+), 219 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts index dc308263baab61..8d00ddb18be6b9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.test.ts @@ -35,32 +35,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([getResult()]); }); - test('should return 2 items over two pages, one per page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.params.immutable = true; - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.params.immutable = true; - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) - ); - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2]); - }); - - test('should return 3 items with over 3 pages one per page', async () => { + test('should return 3 items over 1 page with all on one page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -75,40 +50,17 @@ describe('get_existing_prepackaged_rules', () => { result3.params.immutable = true; result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 1, + page: 1, + total: 3, + }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getExistingPrepackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.params.immutable = true; - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.params.immutable = true; - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.params.immutable = true; - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], @@ -137,7 +89,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([getResult()]); }); - test('should return 2 items over two pages, one per page', async () => { + test('should return 2 items over 1 page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -146,11 +98,19 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 1, + page: 1, + total: 2, + }) ); + + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) + getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) ); const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; @@ -160,7 +120,7 @@ describe('get_existing_prepackaged_rules', () => { expect(rules).toEqual([result1, result2]); }); - test('should return 3 items with over 3 pages one per page', async () => { + test('should return 3 items over 1 page with all on one page', async () => { const alertsClient = alertsClientMock.create(); const result1 = getResult(); @@ -172,37 +132,17 @@ describe('get_existing_prepackaged_rules', () => { const result3 = getResult(); result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) + getFindResultWithMultiHits({ + data: [result1], + perPage: 3, + page: 1, + total: 3, + }) ); - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getNonPackagedRules({ - alertsClient: unsafeCast, - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // second mock which will return all the data on a single page alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ data: [result1, result2, result3], @@ -241,80 +181,27 @@ describe('get_existing_prepackaged_rules', () => { const result2 = getResult(); result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 2 }) - ); - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 2 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); - expect(rules).toEqual([result1, result2]); - }); - - test('should return 3 items with over 3 pages one per page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result1], perPage: 1, page: 1, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result2], perPage: 1, page: 2, total: 3 }) - ); - - alertsClient.find.mockResolvedValueOnce( - getFindResultWithMultiHits({ data: [result3], perPage: 1, page: 2, total: 3 }) - ); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rules = await getRules({ - alertsClient: unsafeCast, - filter: '', - }); - expect(rules).toEqual([result1, result2, result3]); - }); - - test('should return 3 items over 1 pages with all on one page', async () => { - const alertsClient = alertsClientMock.create(); - - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - - const result3 = getResult(); - result3.id = 'f3e1bf0b-b95f-43da-b1de-5d2f0af2287a'; - + // first result mock which is for returning the total alertsClient.find.mockResolvedValueOnce( getFindResultWithMultiHits({ - data: [result1, result2, result3], - perPage: 3, + data: [result1], + perPage: 1, page: 1, - total: 3, + total: 2, }) ); + // second mock which will return all the data on a single page + alertsClient.find.mockResolvedValueOnce( + getFindResultWithMultiHits({ data: [result1, result2], perPage: 2, page: 1, total: 2 }) + ); + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; const rules = await getRules({ alertsClient: unsafeCast, filter: '', }); - expect(rules).toEqual([result1, result2, result3]); + expect(rules).toEqual([result1, result2]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts index b7ab6a97634a80..a48957da7aa942 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_existing_prepackaged_rules.ts @@ -9,7 +9,6 @@ import { AlertsClient } from '../../../../../alerting'; import { RuleAlertType, isAlertTypes } from './types'; import { findRules } from './find_rules'; -export const DEFAULT_PER_PAGE = 100; export const FILTER_NON_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:false"`; export const FILTER_PREPACKED_RULES = `alert.attributes.tags: "${INTERNAL_IMMUTABLE_KEY}:true"`; @@ -33,84 +32,56 @@ export const getRulesCount = async ({ filter, perPage: 1, page: 1, + sortField: 'createdAt', + sortOrder: 'desc', }); return firstRule.total; }; export const getRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, filter, }: { alertsClient: AlertsClient; - perPage?: number; filter: string; }): Promise<RuleAlertType[]> => { - const firstPrepackedRules = await findRules({ + const count = await getRulesCount({ alertsClient, filter }); + const rules = await findRules({ alertsClient, filter, - perPage, + perPage: count, page: 1, + sortField: 'createdAt', + sortOrder: 'desc', }); - const totalPages = Math.ceil(firstPrepackedRules.total / firstPrepackedRules.perPage); - if (totalPages <= 1) { - if (isAlertTypes(firstPrepackedRules.data)) { - return firstPrepackedRules.data; - } else { - // If this was ever true, you have a really messed up system. - // This is keep typescript happy since we have an unknown with data - return []; - } - } else { - const returnPrepackagedRules = await Array(totalPages - 1) - .fill({}) - .map((_, page) => { - // page index starts at 2 as we already got the first page and we have more pages to go - return findRules({ - alertsClient, - filter, - perPage, - page: page + 2, - }); - }) - .reduce<Promise<object[]>>(async (accum, nextPage) => { - return [...(await accum), ...(await nextPage).data]; - }, Promise.resolve(firstPrepackedRules.data)); - if (isAlertTypes(returnPrepackagedRules)) { - return returnPrepackagedRules; - } else { - // If this was ever true, you have a really messed up system. - // This is keep typescript happy since we have an unknown with data - return []; - } + if (isAlertTypes(rules.data)) { + return rules.data; + } else { + // If this was ever true, you have a really messed up system. + // This is keep typescript happy since we have an unknown with data + return []; } }; export const getNonPackagedRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<RuleAlertType[]> => { return getRules({ alertsClient, - perPage, filter: FILTER_NON_PREPACKED_RULES, }); }; export const getExistingPrepackagedRules = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<RuleAlertType[]> => { return getRules({ alertsClient, - perPage, filter: FILTER_PREPACKED_RULES, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts index 0f973d816917fc..02456732df3b4e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts @@ -9,8 +9,6 @@ import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { AlertsClient } from '../../../../../alerting'; import { findRules } from '../rules/find_rules'; -const DEFAULT_PER_PAGE: number = 1000; - export interface TagType { id: string; tags: string[]; @@ -42,39 +40,37 @@ export const convertTagsToSet = (tagObjects: object[]): Set<string> => { // Ref: https://www.elastic.co/guide/en/kibana/master/saved-objects-api.html export const readTags = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; - perPage?: number; }): Promise<string[]> => { - const tags = await readRawTags({ alertsClient, perPage }); + const tags = await readRawTags({ alertsClient }); return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); }; export const readRawTags = async ({ alertsClient, - perPage = DEFAULT_PER_PAGE, }: { alertsClient: AlertsClient; perPage?: number; }): Promise<string[]> => { - const firstTags = await findRules({ alertsClient, fields: ['tags'], perPage, page: 1 }); - const firstSet = convertTagsToSet(firstTags.data); - const totalPages = Math.ceil(firstTags.total / firstTags.perPage); - if (totalPages <= 1) { - return Array.from(firstSet); - } else { - const returnTags = await Array(totalPages - 1) - .fill({}) - .map((_, page) => { - // page index starts at 2 as we already got the first page and we have more pages to go - return findRules({ alertsClient, fields: ['tags'], perPage, page: page + 2 }); - }) - .reduce<Promise<Set<string>>>(async (accum, nextTagPage) => { - const tagArray = convertToTags((await nextTagPage).data); - return new Set([...(await accum), ...tagArray]); - }, Promise.resolve(firstSet)); - - return Array.from(returnTags); - } + // Get just one record so we can get the total count + const firstTags = await findRules({ + alertsClient, + fields: ['tags'], + perPage: 1, + page: 1, + sortField: 'createdAt', + sortOrder: 'desc', + }); + // Get all the rules to aggregate over all the tags of the rules + const rules = await findRules({ + alertsClient, + fields: ['tags'], + perPage: firstTags.total, + sortField: 'createdAt', + sortOrder: 'desc', + page: 1, + }); + const tagSet = convertTagsToSet(rules.data); + return Array.from(tagSet); }; From 653c28a8895e08665f35e8c88c8fc1413f67bff4 Mon Sep 17 00:00:00 2001 From: Andrew Cholakian <andrew@andrewvc.com> Date: Mon, 3 Feb 2020 20:09:56 -0600 Subject: [PATCH 62/62] [Uptime] Add unit tests for QueryContext time calculation (#56671) Add Unit tests for the QueryContext class that was missing testing. This would have caught #56612 --- .../search/__tests__/query_context.test.ts | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts new file mode 100644 index 00000000000000..8924d07ac0c4dc --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/__tests__/query_context.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { QueryContext } from '../query_context'; +import { CursorPagination } from '../..'; +import { CursorDirection, SortOrder } from '../../../../../../common/graphql/types'; + +describe(QueryContext, () => { + // 10 minute range + const rangeStart = '2019-02-03T19:06:54.939Z'; + const rangeEnd = '2019-02-03T19:16:54.939Z'; + + const pagination: CursorPagination = { + cursorDirection: CursorDirection.AFTER, + sortOrder: SortOrder.DESC, + }; + + let qc: QueryContext; + beforeEach(() => (qc = new QueryContext({}, rangeStart, rangeEnd, pagination, null, 10))); + + describe('dateRangeFilter()', () => { + const expectedRange = { + range: { + '@timestamp': { + gte: rangeStart, + lte: rangeEnd, + }, + }, + }; + describe('when hasTimespan() is true', () => { + it('should create a date range filter including the timespan', async () => { + const mockHasTimespan = jest.fn(); + mockHasTimespan.mockReturnValue(true); + qc.hasTimespan = mockHasTimespan; + + expect(await qc.dateRangeFilter()).toEqual({ + bool: { + filter: [ + expectedRange, + { + bool: { + should: [ + qc.timespanClause(), + { bool: { must_not: { exists: { field: 'monitor.timespan' } } } }, + ], + }, + }, + ], + }, + }); + }); + }); + + describe('when hasTimespan() is false', () => { + it('should only use the timestamp fields in the returned filter', async () => { + const mockHasTimespan = jest.fn(); + mockHasTimespan.mockReturnValue(false); + qc.hasTimespan = mockHasTimespan; + + expect(await qc.dateRangeFilter()).toEqual(expectedRange); + }); + }); + }); + + describe('timespanClause()', () => { + it('should always cover the last 5m', () => { + // 5m expected range between GTE and LTE in the response + // since timespan is hardcoded to 5m + expect(qc.timespanClause()).toEqual({ + range: { + 'monitor.timespan': { + // end date minus 5m + gte: new Date(Date.parse(rangeEnd) - 5 * 60 * 1000).toISOString(), + lte: rangeEnd, + }, + }, + }); + }); + }); +});
- + ; -export const MonitorChartsComponent = ({ - data, - mean, - range, - monitorId, - dateRangeStart, - dateRangeEnd, - loading, -}: Props) => { - const [getUrlParams] = useUrlParams(); +export const MonitorChartsComponent = ({ data, mean, range, monitorId, loading }: Props) => { if (data && data.monitorChartsData) { const { monitorChartsData: { locationDurationLines }, } = data; - const { absoluteDateRangeStart, absoluteDateRangeEnd } = getUrlParams(); - return ( @@ -58,15 +44,7 @@ export const MonitorChartsComponent = ({ /> - + ); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap index f779efca7b18a7..3655db5aaff1ea 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_list.test.tsx.snap @@ -106,6 +106,560 @@ exports[`MonitorList component renders a no items message when no data is provid `; exports[`MonitorList component renders the monitor list 1`] = ` +.c1 { + padding-left: 17px; +} + +.c2 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width:574px) { + .c0 { + min-width: 230px; + } +} + +
+
+ Monitor status +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Status + +
+
+
+ + Name + +
+
+
+ + Url + +
+
+
+ + Downtime history + +
+
+
+ +
+
+
+ Status +
+
+
+
+
+
+
+ +
+
+
+
+ + +
+
+ 1897 Yr ago +
+
+
+
+
+
+
+ in 0/1 Location +
+
+
+
+
+
+ Name +
+ +
+
+ Url +
+
+ +
+
+
+ +
+
+ -- +
+
+
+
+
+
+ +
+
+
+ Status +
+
+
+
+
+
+
+ +
+
+
+
+ + +
+
+ 1895 Yr ago +
+
+
+
+
+
+
+ in 1/1 Location +
+
+
+
+
+
+ Name +
+ +
+
+ Url +
+
+ +
+
+
+ +
+
+ -- +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+`; + +exports[`MonitorList component shallow renders the monitor list 1`] = ` `; -exports[`MonitorList component renders the monitor list 1`] = ` +exports[`MonitorListPagination component renders the monitor list 1`] = ` { let result: MonitorSummaryResult; @@ -81,11 +82,9 @@ describe('MonitorList component', () => { }; }); - it('renders the monitor list', () => { + it('shallow renders the monitor list', () => { const component = shallowWithIntl( { it('renders a no items message when no data is provided', () => { const component = shallowWithIntl( { ); expect(component).toMatchSnapshot(); }); + + it('renders the monitor list', () => { + const component = renderWithIntl( + renderWithRouter( + + ) + ); + + expect(component).toMatchSnapshot(); + }); }); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx index a1725134094559..ff54e61006156b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/monitor_list_pagination.test.tsx @@ -13,7 +13,7 @@ import { } from '../../../../../common/graphql/types'; import { MonitorListComponent } from '../monitor_list'; -describe('MonitorList component', () => { +describe('MonitorListPagination component', () => { let result: MonitorSummaryResult; beforeEach(() => { @@ -98,8 +98,6 @@ describe('MonitorList component', () => { it('renders the monitor list', () => { const component = shallowWithIntl( { it('renders a no items message when no data is provided', () => { const component = shallowWithIntl( { - const { - absoluteStartDate, - absoluteEndDate, - dangerColor, - data, - errors, - hasActiveFilters, - linkParameters, - loading, - } = props; + const { dangerColor, data, errors, hasActiveFilters, linkParameters, loading } = props; const [drawerIds, updateDrawerIds] = useState([]); const items = data?.monitorStates?.summaries ?? []; @@ -132,12 +121,7 @@ export const MonitorListComponent = (props: Props) => { show: false, }, render: (histogramSeries: SummaryHistogramPoint[] | null) => ( - + ), }, { diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/__snapshots__/monitor_status.bar.test.tsx.snap diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx similarity index 79% rename from x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx rename to x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx index 545405f91d5374..0a53eeb89d793f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/__tests__/monitor_status.bar.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/__test__/monitor_status.bar.test.tsx @@ -7,14 +7,12 @@ import moment from 'moment'; import React from 'react'; import { renderWithIntl } from 'test_utils/enzyme_helpers'; -import { Ping } from '../../../../common/graphql/types'; -import { MonitorStatusBarComponent } from '../monitor_status_details/monitor_status_bar'; +import { MonitorStatusBarComponent } from '../monitor_status_bar'; +import { Ping } from '../../../../../common/graphql/types'; describe('MonitorStatusBar component', () => { let monitorStatus: Ping; let monitorLocations: any; - let dateStart: string; - let dateEnd: string; beforeEach(() => { monitorStatus = { @@ -46,9 +44,6 @@ describe('MonitorStatusBar component', () => { }, ], }; - - dateStart = moment('01-01-2010').toString(); - dateEnd = moment('10-10-2010').toString(); }); it('renders duration in ms, not us', () => { @@ -56,10 +51,7 @@ describe('MonitorStatusBar component', () => { ); expect(component).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts index 7b4e1ea353c11c..385788cc825a0a 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts @@ -3,34 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { AppState } from '../../../state'; -import { selectMonitorLocations } from '../../../state/selectors'; -import { fetchMonitorLocations } from '../../../state/actions/monitor'; -import { MonitorStatusDetailsComponent } from './monitor_status_details'; -const mapStateToProps = (state: AppState, { monitorId }: any) => ({ - monitorLocations: selectMonitorLocations(state, monitorId), -}); - -const mapDispatchToProps = (dispatch: any, ownProps: any) => ({ - loadMonitorLocations: () => { - const { dateStart, dateEnd, monitorId } = ownProps; - dispatch( - fetchMonitorLocations({ - monitorId, - dateStart, - dateEnd, - }) - ); - }, -}); - -export const MonitorStatusDetails = connect( - mapStateToProps, - mapDispatchToProps -)(MonitorStatusDetailsComponent); - -export * from './monitor_status_details'; -export { MonitorStatusBar } from './monitor_status_bar'; +export { MonitorStatusBarComponent } from './monitor_status_bar'; +export { MonitorStatusDetailsComponent } from './monitor_status_details'; export { StatusByLocations } from './monitor_status_bar/status_by_location'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts index 94bd7fa7f026bc..0cb11587eee480 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/index.ts @@ -4,47 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { - StateProps, - DispatchProps, - MonitorStatusBarComponent, - MonitorStatusBarProps, -} from './monitor_status_bar'; -import { selectMonitorStatus, selectMonitorLocations } from '../../../../state/selectors'; -import { AppState } from '../../../../state'; -import { getMonitorStatus, getSelectedMonitor } from '../../../../state/actions'; - -const mapStateToProps = (state: AppState, ownProps: MonitorStatusBarProps) => ({ - monitorStatus: selectMonitorStatus(state), - monitorLocations: selectMonitorLocations(state, ownProps.monitorId), -}); - -const mapDispatchToProps = (dispatch: Dispatch, ownProps: MonitorStatusBarProps) => ({ - loadMonitorStatus: () => { - const { dateStart, dateEnd, monitorId } = ownProps; - dispatch( - getMonitorStatus({ - monitorId, - dateStart, - dateEnd, - }) - ); - dispatch( - getSelectedMonitor({ - monitorId, - }) - ); - }, -}); - -// @ts-ignore TODO: Investigate typescript issues here -export const MonitorStatusBar = connect( - // @ts-ignore TODO: Investigate typescript issues here - mapStateToProps, - mapDispatchToProps -)(MonitorStatusBarComponent); - export { MonitorSSLCertificate } from './monitor_ssl_certificate'; -export * from './monitor_status_bar'; +export { MonitorStatusBarComponent } from './monitor_status_bar'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx index 2524039829add1..22e4377944be13 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_bar/monitor_status_bar.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { EuiLink, EuiTitle, @@ -13,42 +14,23 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import React, { useEffect } from 'react'; import { MonitorSSLCertificate } from './monitor_ssl_certificate'; import * as labels from './translations'; import { StatusByLocations } from './status_by_location'; import { Ping } from '../../../../../common/graphql/types'; import { MonitorLocations } from '../../../../../common/runtime_types'; -export interface StateProps { +interface MonitorStatusBarProps { + monitorId: string; monitorStatus: Ping; monitorLocations: MonitorLocations; } -export interface DispatchProps { - loadMonitorStatus: () => void; -} - -export interface MonitorStatusBarProps { - monitorId: string; - dateStart: string; - dateEnd: string; -} - -type Props = MonitorStatusBarProps & StateProps & DispatchProps; - -export const MonitorStatusBarComponent: React.FC = ({ - dateStart, - dateEnd, +export const MonitorStatusBarComponent: React.FC = ({ monitorId, - loadMonitorStatus, monitorStatus, monitorLocations, }) => { - useEffect(() => { - loadMonitorStatus(); - }, [dateStart, dateEnd, loadMonitorStatus]); - const full = monitorStatus?.url?.full ?? ''; return ( diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx index 29bd8eb3a71837..7dea73da7bba0e 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx @@ -8,16 +8,13 @@ import React, { useContext, useEffect, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import styled from 'styled-components'; import { LocationMap } from '../location_map'; -import { MonitorStatusBar } from './monitor_status_bar'; import { UptimeRefreshContext } from '../../../contexts'; +import { MonitorLocations } from '../../../../common/runtime_types'; +import { MonitorStatusBar } from '../../connected'; -interface MonitorStatusBarProps { +interface MonitorStatusDetailsProps { monitorId: string; - variables: any; - loadMonitorLocations: any; - monitorLocations: any; - dateStart: any; - dateEnd: any; + monitorLocations: MonitorLocations; } const WrapFlexItem = styled(EuiFlexItem)` @@ -28,15 +25,8 @@ const WrapFlexItem = styled(EuiFlexItem)` export const MonitorStatusDetailsComponent = ({ monitorId, - variables, - loadMonitorLocations, monitorLocations, - dateStart, - dateEnd, -}: MonitorStatusBarProps) => { - useEffect(() => { - loadMonitorLocations(monitorId); - }, [loadMonitorLocations, monitorId, dateStart, dateEnd]); +}: MonitorStatusDetailsProps) => { const { refreshApp } = useContext(UptimeRefreshContext); const [isTabActive] = useState(document.visibilityState); @@ -63,12 +53,7 @@ export const MonitorStatusDetailsComponent = ({ - + diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx index 90d716001cff96..8531cd1a3cc83d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/snapshot.tsx @@ -5,63 +5,28 @@ */ import { EuiSpacer } from '@elastic/eui'; -import React, { useEffect } from 'react'; +import React from 'react'; import { get } from 'lodash'; -import { connect } from 'react-redux'; -import { Snapshot as SnapshotType } from '../../../common/runtime_types'; import { DonutChart } from './charts'; -import { fetchSnapshotCount } from '../../state/actions'; import { ChartWrapper } from './charts/chart_wrapper'; import { SnapshotHeading } from './snapshot_heading'; -import { AppState } from '../../state'; +import { Snapshot as SnapshotType } from '../../../common/runtime_types'; const SNAPSHOT_CHART_WIDTH = 144; const SNAPSHOT_CHART_HEIGHT = 144; -/** - * Props expected from parent components. - */ -interface OwnProps { - dateRangeStart: string; - dateRangeEnd: string; - filters?: string; - /** - * Height is needed, since by default charts takes height of 100% - */ - height?: string; - statusFilter?: string; -} - -/** - * Props given by the Redux store based on action input. - */ -interface StoreProps { +interface SnapshotComponentProps { count: SnapshotType; - lastRefresh: number; loading: boolean; + height?: string; } /** - * Contains functions that will dispatch actions used - * for this component's life cycle - */ -interface DispatchProps { - loadSnapshotCount: typeof fetchSnapshotCount; -} - -/** - * Props used to render the Snapshot component. + * This component visualizes a KPI and histogram chart to help users quickly + * glean the status of their uptime environment. + * @param props the props required by the component */ -type Props = OwnProps & StoreProps & DispatchProps; - -type PresentationalComponentProps = Pick & - Pick; - -export const PresentationalComponent: React.FC = ({ - count, - height, - loading, -}) => ( +export const SnapshotComponent: React.FC = ({ count, height, loading }) => ( (count, 'down', 0)} total={get(count, 'total', 0)} /> @@ -73,59 +38,3 @@ export const PresentationalComponent: React.FC = ( /> ); - -/** - * This component visualizes a KPI and histogram chart to help users quickly - * glean the status of their uptime environment. - * @param props the props required by the component - */ -export const Container: React.FC = ({ - count, - dateRangeStart, - dateRangeEnd, - filters, - height, - statusFilter, - lastRefresh, - loading, - loadSnapshotCount, -}: Props) => { - useEffect(() => { - loadSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter); - }, [dateRangeStart, dateRangeEnd, filters, lastRefresh, loadSnapshotCount, statusFilter]); - return ; -}; - -/** - * Provides state to connected component. - * @param state the root app state - */ -const mapStateToProps = ({ - snapshot: { count, loading }, - ui: { lastRefresh }, -}: AppState): StoreProps => ({ - count, - lastRefresh, - loading, -}); - -/** - * Used for fetching snapshot counts. - * @param dispatch redux-provided action dispatcher - */ -const mapDispatchToProps = (dispatch: any) => ({ - loadSnapshotCount: ( - dateRangeStart: string, - dateRangeEnd: string, - filters?: string, - statusFilter?: string - ): DispatchProps => { - return dispatch(fetchSnapshotCount(dateRangeStart, dateRangeEnd, filters, statusFilter)); - }, -}); - -export const Snapshot = connect( - // @ts-ignore connect is expecting null | undefined for some reason - mapStateToProps, - mapDispatchToProps -)(Container); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx index 03ab9fb5cf194b..2c0be2aa15d6fb 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/status_panel.tsx @@ -4,52 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import React from 'react'; -import { Snapshot } from './snapshot'; -import { PingHistogram } from '../connected'; - -interface StatusPanelProps { - absoluteDateRangeStart: number; - absoluteDateRangeEnd: number; - dateRangeStart: string; - dateRangeEnd: string; - filters?: string; - statusFilter?: string; -} +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { PingHistogram, Snapshot } from '../connected'; const STATUS_CHART_HEIGHT = '160px'; -export const StatusPanel = ({ - absoluteDateRangeStart, - absoluteDateRangeEnd, - dateRangeStart, - dateRangeEnd, - filters, - statusFilter, -}: StatusPanelProps) => ( +export const StatusPanel = ({}) => ( - + - + diff --git a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts index d02a6fc2afb5d2..8c9eec3abe2239 100644 --- a/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts +++ b/x-pack/legacy/plugins/uptime/public/hooks/update_kuery_string.ts @@ -55,9 +55,9 @@ export const useUpdateKueryString = ( const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, indexPattern); esFilters = JSON.stringify(elasticsearchQuery); - - updateEsQueryForFilterGroup(filterQueryString, indexPattern); } + updateEsQueryForFilterGroup(filterQueryString, indexPattern); + return [esFilters]; } catch (err) { return [urlFilters, err]; diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 408d2584911e0d..a8501ff14313a8 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -12,8 +12,8 @@ import { UMUpdateBreadcrumbs } from '../lib/lib'; import { UptimeRefreshContext, UptimeThemeContext } from '../contexts'; import { useUptimeTelemetry, useUrlParams, UptimePage } from '../hooks'; import { useTrackPageview } from '../../../infra/public'; -import { MonitorStatusDetails } from '../components/functional/monitor_status_details'; import { PageHeader } from './page_header'; +import { MonitorStatusDetails } from '../components/connected'; interface MonitorPageProps { setBreadcrumbs: UMUpdateBreadcrumbs; @@ -49,20 +49,9 @@ export const MonitorPage = ({ setBreadcrumbs }: MonitorPageProps) => { - + - + } - + Date: Mon, 3 Feb 2020 10:12:32 -0600 Subject: [PATCH 46/62] [SIEM] Use Core HTTP Client (#54210) * Replace uses of chrome.getBasePath and fetch with core.http While core.http is coming from 'above' these functions, it doesn't make a lot of sense to pass the client through the entire stack to be able to call it at the bottom, because longer-term we'll abstract these http calls with some redux middleware. In the short term we have a mechanism to refer to core through a singleton, 'ui/new_platform', which should be around until 8.0 at least. Ideally, we'll have a more robust architecture in place by then. If not, we can reproduce the singleton module ourselves. * Fix index patterns API call core.http.fetch doesn't like a querystring in the path argument, so we move it to the query object instead. The 'type' field specifier is redundant as the type is always returned (and not as an attribute). * Refactor getIndexPatterns to use the savedObjects client We lose the updated_at field by using the client, but we weren't actually using it. I don't _think_ that the savedObjects client supports request aborts right now, but when it does we'll get that back for free. * Pass query params as object to core.http A request with query params in its path does not properly encode said params (the '?', at the very least), leading to malformed requests that result in 404s. * Remove redundant API logic This function was originally used to query both an individual rule and a list of rules, but the former functionality has been moved to fetchRuleById. * Allow throwIfNotOk to handle an undefined response This is also an error for us, and we throw as such.. * Convert new Rules APIs to use core.http These all occurred on master, this fixes them post-merge. * Refactor Signals requests expecting custom Errors These requests package up error responses in custom errors, which callers are expecting. We should refactor all of these calls to behave similarly, but for now let's just not break existing ones. * Remove default credentials specification The default is credentials: 'same-origin', and so we can omit it. * Update types in new uses of hook This savedObject type is slightly modified now that the hook is using the NP savedObjects client. * Replace explicit system header with fetch option The asSystemRequest option accomplishes the same thing without requiring us to know the implementation. With the addition of this option, setting this header explicitly causes an error. This also removes the credentials: same-origin specifier as it is the default. * Remove redundant awaits The response has previously been resolved, and so our body should be populated, here. Co-authored-by: Elastic Machine --- .../components/embeddables/__mocks__/mock.ts | 20 +- .../embeddables/embedded_map_helpers.tsx | 2 +- .../components/ml/api/anomalies_table_data.ts | 30 +-- .../components/ml/api/get_ml_capabilities.ts | 17 +- .../siem/public/components/ml_popover/api.tsx | 191 ++++++++------- .../public/components/ml_popover/types.ts | 10 - .../containers/detection_engine/rules/api.ts | 223 +++++++----------- .../detection_engine/rules/types.ts | 9 + .../detection_engine/signals/api.ts | 123 ++++------ .../siem/public/hooks/api/__mock__/api.tsx | 6 +- .../plugins/siem/public/hooks/api/api.test.ts | 4 + .../plugins/siem/public/hooks/api/api.tsx | 43 ++-- .../legacy/plugins/siem/public/hooks/types.ts | 23 +- .../siem/public/hooks/use_index_patterns.tsx | 8 +- x-pack/legacy/plugins/siem/public/plugin.tsx | 1 + 15 files changed, 299 insertions(+), 411 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts index 7834bb4511dc64..19ad0d452feb17 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__mocks__/mock.ts @@ -5,7 +5,7 @@ */ import { IndexPatternMapping } from '../types'; -import { IndexPatternSavedObject } from '../../ml_popover/types'; +import { IndexPatternSavedObject } from '../../../hooks/types'; export const mockIndexPatternIds: IndexPatternMapping[] = [ { title: 'filebeat-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, @@ -425,8 +425,7 @@ export const mockLayerListMixed = [ export const mockAPMIndexPattern: IndexPatternSavedObject = { id: 'apm-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-*', }, @@ -435,8 +434,7 @@ export const mockAPMIndexPattern: IndexPatternSavedObject = { export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { id: 'apm-7.*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-7.*', }, @@ -445,8 +443,7 @@ export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { export const mockFilebeatIndexPattern: IndexPatternSavedObject = { id: 'filebeat-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'filebeat-*', }, @@ -455,8 +452,7 @@ export const mockFilebeatIndexPattern: IndexPatternSavedObject = { export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { id: 'auditbeat-*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'auditbeat-*', }, @@ -465,8 +461,7 @@ export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { id: 'apm-*-transaction*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: 'apm-*-transaction*', }, @@ -475,8 +470,7 @@ export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { export const mockGlobIndexPattern: IndexPatternSavedObject = { id: '*', type: 'index-pattern', - updated_at: '', - version: 'abc', + _version: 'abc', attributes: { title: '*', }, 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 2d4714401f3b31..e370cbbf64a4a1 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 @@ -21,7 +21,7 @@ import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/common/constants'; import * as i18n from './translations'; import { Query, esFilters } from '../../../../../../../src/plugins/data/public'; -import { IndexPatternSavedObject } from '../ml_popover/types'; +import { IndexPatternSavedObject } from '../../hooks/types'; /** * Creates MapEmbeddable with provided initial configuration diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts index 10b2538d1e7852..cb84d9182d2e01 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/anomalies_table_data.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - +import { npStart } from 'ui/new_platform'; import { Anomalies, InfluencerInput, CriteriaFields } from '../types'; import { throwIfNotOk } from '../../../hooks/api/api'; + export interface Body { jobIds: string[]; criteriaFields: CriteriaFields[]; @@ -22,17 +22,17 @@ export interface Body { } export const anomaliesTableData = async (body: Body, signal: AbortSignal): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/results/anomalies_table_data`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify(body), - headers: { - 'content-Type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - }); - await throwIfNotOk(response); - return response.json(); + const response = await npStart.core.http.fetch( + '/api/ml/results/anomalies_table_data', + { + method: 'POST', + body: JSON.stringify(body), + asResponse: true, + asSystemRequest: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts index 13339510284945..dcfd7365f8e0d5 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml/api/get_ml_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { InfluencerInput, MlCapabilities } from '../types'; import { throwIfNotOk } from '../../../hooks/api/api'; @@ -23,16 +23,13 @@ export interface Body { } export const getMlCapabilities = async (signal: AbortSignal): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/ml_capabilities`, { + const response = await npStart.core.http.fetch('/api/ml/ml_capabilities', { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-Type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, }); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx index a04b8f4b996536..cf939d8e09b7ec 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/api.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; + import { CheckRecognizerProps, CloseJobsResponse, @@ -31,21 +32,18 @@ export const checkRecognizer = async ({ indexPatternName, signal, }: CheckRecognizerProps): Promise => { - const response = await fetch( - `${chrome.getBasePath()}/api/ml/modules/recognize/${indexPatternName}`, + const response = await npStart.core.http.fetch( + `/api/ml/modules/recognize/${indexPatternName}`, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, } ); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -55,18 +53,18 @@ export const checkRecognizer = async ({ * @param signal to cancel request */ export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/get_module/${moduleId}`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - }); - await throwIfNotOk(response); - return response.json(); + const response = await npStart.core.http.fetch( + `/api/ml/modules/get_module/${moduleId}`, + { + method: 'GET', + asResponse: true, + asSystemRequest: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -77,7 +75,6 @@ export const getModules = async ({ moduleId = '', signal }: GetModulesProps): Pr * @param jobIdErrorFilter - if provided, filters all errors except for given jobIds * @param groups - list of groups to add to jobs being installed * @param prefix - prefix to be added to job name - * @param headers optional headers to add */ export const setupMlJob = async ({ configTemplate, @@ -86,25 +83,26 @@ export const setupMlJob = async ({ groups = ['siem'], prefix = '', }: MlSetupArgs): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/modules/setup/${configTemplate}`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - prefix, - groups, - indexPatternName, - startDatafeed: false, - useDedicatedIndex: true, - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); - await throwIfNotOk(response); - const json = await response.json(); + const response = await npStart.core.http.fetch( + `/api/ml/modules/setup/${configTemplate}`, + { + method: 'POST', + body: JSON.stringify({ + prefix, + groups, + indexPatternName, + startDatafeed: false, + useDedicatedIndex: true, + }), + asResponse: true, + asSystemRequest: true, + } + ); + + await throwIfNotOk(response.response); + const json = response.body!; throwIfErrorAttachedToSetup(json, jobIdErrorFilter); + return json; }; @@ -121,22 +119,23 @@ export const startDatafeeds = async ({ datafeedIds: string[]; start: number; }): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/force_start_datafeeds`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - datafeedIds, - ...(start !== 0 && { start }), - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); - await throwIfNotOk(response); - const json = await response.json(); + const response = await npStart.core.http.fetch( + '/api/ml/jobs/force_start_datafeeds', + { + method: 'POST', + body: JSON.stringify({ + datafeedIds, + ...(start !== 0 && { start }), + }), + asResponse: true, + asSystemRequest: true, + } + ); + + await throwIfNotOk(response.response); + const json = response.body!; throwIfErrorAttached(json, datafeedIds); + return json; }; @@ -144,49 +143,46 @@ export const startDatafeeds = async ({ * Stops the given dataFeedIds and sets the corresponding Job's jobState to closed * * @param datafeedIds - * @param headers optional headers to add */ export const stopDatafeeds = async ({ datafeedIds, }: { datafeedIds: string[]; }): Promise<[StopDatafeedResponse | ErrorResponse, CloseJobsResponse]> => { - const stopDatafeedsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/stop_datafeeds`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - datafeedIds, - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); + const stopDatafeedsResponse = await npStart.core.http.fetch( + '/api/ml/jobs/stop_datafeeds', + { + method: 'POST', + body: JSON.stringify({ + datafeedIds, + }), + asResponse: true, + asSystemRequest: true, + } + ); - await throwIfNotOk(stopDatafeedsResponse); - const stopDatafeedsResponseJson = await stopDatafeedsResponse.json(); + await throwIfNotOk(stopDatafeedsResponse.response); + const stopDatafeedsResponseJson = stopDatafeedsResponse.body!; const datafeedPrefix = 'datafeed-'; - const closeJobsResponse = await fetch(`${chrome.getBasePath()}/api/ml/jobs/close_jobs`, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - jobIds: datafeedIds.map(dataFeedId => - dataFeedId.startsWith(datafeedPrefix) - ? dataFeedId.substring(datafeedPrefix.length) - : dataFeedId - ), - }), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - }); + const closeJobsResponse = await npStart.core.http.fetch( + '/api/ml/jobs/close_jobs', + { + method: 'POST', + body: JSON.stringify({ + jobIds: datafeedIds.map(dataFeedId => + dataFeedId.startsWith(datafeedPrefix) + ? dataFeedId.substring(datafeedPrefix.length) + : dataFeedId + ), + }), + asResponse: true, + asSystemRequest: true, + } + ); - await throwIfNotOk(closeJobsResponse); - return [stopDatafeedsResponseJson, await closeJobsResponse.json()]; + await throwIfNotOk(closeJobsResponse.response); + return [stopDatafeedsResponseJson, closeJobsResponse.body!]; }; /** @@ -198,17 +194,14 @@ export const stopDatafeeds = async ({ * @param signal to cancel request */ export const getJobsSummary = async (signal: AbortSignal): Promise => { - const response = await fetch(`${chrome.getBasePath()}/api/ml/jobs/jobs_summary`, { + const response = await npStart.core.http.fetch('/api/ml/jobs/jobs_summary', { method: 'POST', - credentials: 'same-origin', body: JSON.stringify({}), - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, + asResponse: true, + asSystemRequest: true, signal, }); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts index 964ae8c8242d4b..f3bf78fdbb94c5 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/types.ts @@ -193,16 +193,6 @@ export interface CloseJobsResponse { }; } -export interface IndexPatternSavedObject { - attributes: { - title: string; - }; - id: string; - type: string; - updated_at: string; - version: string; -} - export interface JobsFilters { filterQuery: string; showCustomJobs: boolean; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 8f4abeb31c2269..4f50a9bd147881 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; + import { AddRulesProps, DeleteRulesProps, @@ -19,8 +20,9 @@ import { ImportRulesProps, ExportRulesProps, RuleError, - RuleStatus, + RuleStatusResponse, ImportRulesResponse, + PrePackagedRulesStatusResponse, } from './types'; import { throwIfNotOk } from '../../../hooks/api/api'; import { @@ -39,19 +41,15 @@ import * as i18n from '../../../pages/detection_engine/rules/translations'; * @param signal to cancel request */ export const addRule = async ({ rule, signal }: AddRulesProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_RULES_URL, { method: rule.id != null ? 'PUT' : 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify(rule), + asResponse: true, signal, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -79,40 +77,36 @@ export const fetchRules = async ({ signal, }: FetchRulesProps): Promise => { const filters = [ - ...(filterOptions.filter.length !== 0 - ? [`alert.attributes.name:%20${encodeURIComponent(filterOptions.filter)}`] - : []), + ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), ...(filterOptions.showCustomRules - ? ['alert.attributes.tags:%20%22__internal_immutable:false%22'] + ? [`alert.attributes.tags: "__internal_immutable:false"`] : []), ...(filterOptions.showElasticRules - ? ['alert.attributes.tags:%20%22__internal_immutable:true%22'] + ? [`alert.attributes.tags: "__internal_immutable:true"`] : []), - ...(filterOptions.tags?.map(t => `alert.attributes.tags:${encodeURIComponent(t)}`) ?? []), + ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), ]; - const queryParams = [ - `page=${pagination.page}`, - `per_page=${pagination.perPage}`, - `sort_field=${filterOptions.sortField}`, - `sort_order=${filterOptions.sortOrder}`, - ...(filters.length > 0 ? [`filter=${filters.join('%20AND%20')}`] : []), - ]; + const query = { + page: pagination.page, + per_page: pagination.perPage, + sort_field: filterOptions.sortField, + sort_order: filterOptions.sortOrder, + ...(filters.length ? { filter: filters.join(' AND ') } : {}), + }; - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_find?${queryParams.join('&')}`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_find`, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -123,18 +117,15 @@ export const fetchRules = async ({ * */ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}?id=${id}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_RULES_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query: { id }, + asResponse: true, signal, }); - await throwIfNotOk(response); - const rule: Rule = await response.json(); - return rule; + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -146,21 +137,17 @@ export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { method: 'PUT', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify(ids.map(id => ({ id, enabled }))), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -171,21 +158,17 @@ export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { - method: 'DELETE', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + method: 'PUT', body: JSON.stringify(ids.map(id => ({ id }))), + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -194,15 +177,10 @@ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify( rules.map(rule => ({ ...rule, @@ -223,11 +201,12 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_PREPACKAGED_URL, { method: 'PUT', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); + + await throwIfNotOk(response.response); return true; }; @@ -266,21 +242,19 @@ export const importRules = async ({ const formData = new FormData(); formData.append('file', fileToImport); - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_import?overwrite=${overwrite}`, + const response = await npStart.core.http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_import`, { method: 'POST', - credentials: 'same-origin', - headers: { - 'kbn-xsrf': 'true', - }, + query: { overwrite }, body: formData, + asResponse: true, signal, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -304,24 +278,19 @@ export const exportRules = async ({ ? JSON.stringify({ objects: ruleIds.map(rule => ({ rule_id: rule })) }) : undefined; - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_export?exclude_export_details=${excludeExportDetails}&file_name=${encodeURIComponent( - filename - )}`, - { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - body, - signal, - } - ); + const response = await npStart.core.http.fetch(`${DETECTION_ENGINE_RULES_URL}/_export`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + asResponse: true, + }); - await throwIfNotOk(response); - return response.blob(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -338,24 +307,19 @@ export const getRuleStatusById = async ({ }: { id: string; signal: AbortSignal; -}): Promise> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS_URL}?ids=${encodeURIComponent( - JSON.stringify([id]) - )}`, +}): Promise => { + const response = await npStart.core.http.fetch( + DETECTION_ENGINE_RULES_STATUS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, + query: { ids: JSON.stringify([id]) }, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -365,18 +329,14 @@ export const getRuleStatusById = async ({ * */ export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_TAGS_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_TAGS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -390,25 +350,16 @@ export const getPrePackagedRulesStatus = async ({ signal, }: { signal: AbortSignal; -}): Promise<{ - rules_custom_installed: number; - rules_installed: number; - rules_not_installed: number; - rules_not_updated: number; -}> => { - const response = await fetch( - `${chrome.getBasePath()}${DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL}`, +}): Promise => { + const response = await npStart.core.http.fetch( + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, } ); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index b30c3b211b1b8d..0aaffb7b86b284 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -197,3 +197,12 @@ export interface RuleInfoStatus { last_failure_message: string | null; last_success_message: string | null; } + +export type RuleStatusResponse = Record; + +export interface PrePackagedRulesStatusResponse { + rules_custom_installed: number; + rules_installed: number; + rules_not_installed: number; + rules_not_updated: number; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts index 8754d73637e7c7..d0da70e6461248 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/api.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; import { throwIfNotOk } from '../../../hooks/api/api'; import { @@ -14,40 +14,37 @@ import { DETECTION_ENGINE_PRIVILEGES_URL, } from '../../../../common/constants'; import { + BasicSignals, + PostSignalError, + Privilege, QuerySignals, + SignalIndexError, SignalSearchResponse, - UpdateSignalStatusProps, SignalsIndex, - SignalIndexError, - Privilege, - PostSignalError, - BasicSignals, + UpdateSignalStatusProps, } from './types'; -import { parseJsonFromBody } from '../../../utils/api'; /** * Fetch Signals by providing a query * * @param query String to match a dsl - * @param signal AbortSignal for cancelling request */ export const fetchQuerySignals = async ({ query, signal, }: QuerySignals): Promise> => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_QUERY_SIGNALS_URL}`, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - body: JSON.stringify(query), - signal, - }); - await throwIfNotOk(response); - const signals = await response.json(); - return signals; + const response = await npStart.core.http.fetch>( + DETECTION_ENGINE_QUERY_SIGNALS_URL, + { + method: 'POST', + body: JSON.stringify(query), + asResponse: true, + signal, + } + ); + + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -62,19 +59,15 @@ export const updateSignalStatus = async ({ status, signal, }: UpdateSignalStatusProps): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_SIGNALS_STATUS_URL}`, { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, body: JSON.stringify({ status, ...query }), + asResponse: true, signal, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -82,25 +75,18 @@ export const updateSignalStatus = async ({ * * @param signal AbortSignal for cancelling request */ -export const getSignalIndex = async ({ signal }: BasicSignals): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - signal, - }); - if (response.ok) { - const signalIndex = await response.json(); - return signalIndex; +export const getSignalIndex = async ({ signal }: BasicSignals): Promise => { + try { + return await npStart.core.http.fetch(DETECTION_ENGINE_INDEX_URL, { + method: 'GET', + signal, + }); + } catch (e) { + if (e.body) { + throw new SignalIndexError(e.body); + } + throw e; } - const error = await parseJsonFromBody(response); - if (error != null) { - throw new SignalIndexError(error); - } - return null; }; /** @@ -108,19 +94,15 @@ export const getSignalIndex = async ({ signal }: BasicSignals): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_PRIVILEGES_URL}`, { +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => { + const response = await npStart.core.http.fetch(DETECTION_ENGINE_PRIVILEGES_URL, { method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, signal, + asResponse: true, }); - await throwIfNotOk(response); - return response.json(); + await throwIfNotOk(response.response); + return response.body!; }; /** @@ -128,23 +110,16 @@ export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => { - const response = await fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_INDEX_URL}`, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-xsrf': 'true', - }, - signal, - }); - if (response.ok) { - const signalIndex = await response.json(); - return signalIndex; - } - const error = await parseJsonFromBody(response); - if (error != null) { - throw new PostSignalError(error); +export const createSignalIndex = async ({ signal }: BasicSignals): Promise => { + try { + return await npStart.core.http.fetch(DETECTION_ENGINE_INDEX_URL, { + method: 'POST', + signal, + }); + } catch (e) { + if (e.body) { + throw new PostSignalError(e.body); + } + throw e; } - return null; }; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx index 13f53cd34feb62..b12b04e8f760b5 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/__mock__/api.tsx @@ -13,8 +13,7 @@ export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [ attributes: { title: 'filebeat-*', }, - updated_at: '2019-08-26T04:30:09.111Z', - version: 'WzE4LLwxXQ==', + _version: 'WzE4LLwxXQ==', }, { type: 'index-pattern', @@ -22,7 +21,6 @@ export const mockIndexPatternSavedObjects: IndexPatternSavedObject[] = [ attributes: { title: 'auditbeat-*', }, - updated_at: '2019-08-26T04:31:12.934Z', - version: 'WzELLywxXQ==', + _version: 'WzELLywxXQ==', }, ]; diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts index 95825b7d4abda5..208a3b14ca283c 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.test.ts @@ -13,6 +13,10 @@ describe('api', () => { }); describe('#throwIfNotOk', () => { + test('throws a network error if there is no response', async () => { + await expect(throwIfNotOk()).rejects.toThrow('Network Error'); + }); + test('does a throw if it is given response that is not ok and the body is not parsable', async () => { fetchMock.mock('http://example.com', 500); const response = await fetch('http://example.com'); diff --git a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx index 8d319ffe239027..f5f32da7d8c0b2 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/api/api.tsx @@ -4,46 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import chrome from 'ui/chrome'; - +import { npStart } from 'ui/new_platform'; import * as i18n from '../translations'; import { parseJsonFromBody, ToasterErrors } from '../../components/ml/api/throw_if_not_ok'; -import { IndexPatternResponse, IndexPatternSavedObject } from '../types'; - -const emptyIndexPattern: IndexPatternSavedObject[] = []; +import { IndexPatternSavedObject, IndexPatternSavedObjectAttributes } from '../types'; /** * Fetches Configured Index Patterns from the Kibana saved objects API * * TODO: Refactor to context provider: https://github.com/elastic/siem-team/issues/448 - * - * @param signal */ -export const getIndexPatterns = async (signal: AbortSignal): Promise => { - const response = await fetch( - `${chrome.getBasePath()}/api/saved_objects/_find?type=index-pattern&fields=title&fields=type&per_page=10000`, - { - method: 'GET', - credentials: 'same-origin', - headers: { - 'content-type': 'application/json', - 'kbn-system-api': 'true', - 'kbn-xsrf': 'true', - }, - signal, - } - ); - await throwIfNotOk(response); - const results: IndexPatternResponse = await response.json(); +export const getIndexPatterns = async (): Promise => { + const response = await npStart.core.savedObjects.client.find({ + type: 'index-pattern', + fields: ['title'], + perPage: 10000, + }); - if (results.saved_objects && Array.isArray(results.saved_objects)) { - return results.saved_objects; - } else { - return emptyIndexPattern; - } + return response.savedObjects; }; -export const throwIfNotOk = async (response: Response): Promise => { +export const throwIfNotOk = async (response?: Response): Promise => { + if (!response) { + throw new ToasterErrors([i18n.NETWORK_ERROR]); + } + if (!response.ok) { const body = await parseJsonFromBody(response); if (body != null && body.message) { diff --git a/x-pack/legacy/plugins/siem/public/hooks/types.ts b/x-pack/legacy/plugins/siem/public/hooks/types.ts index 4d66d8e1912352..301b8bd6553337 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/types.ts +++ b/x-pack/legacy/plugins/siem/public/hooks/types.ts @@ -4,19 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IndexPatternSavedObject { - attributes: { - title: string; - }; - id: string; - type: string; - updated_at: string; - version: string; -} +import { SimpleSavedObject } from '../../../../../../src/core/public'; -export interface IndexPatternResponse { - page: number; - per_page: number; - saved_objects: IndexPatternSavedObject[]; - total: number; -} +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type IndexPatternSavedObjectAttributes = { title: string }; + +export type IndexPatternSavedObject = Pick< + SimpleSavedObject, + 'type' | 'id' | 'attributes' | '_version' +>; diff --git a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx index 7abe88402096c8..35bed69e8617eb 100644 --- a/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx +++ b/x-pack/legacy/plugins/siem/public/hooks/use_index_patterns.tsx @@ -8,10 +8,10 @@ import { useEffect, useState } from 'react'; import { useStateToaster } from '../components/toasters'; import { errorToToaster } from '../components/ml/api/error_to_toaster'; -import { IndexPatternSavedObject } from '../components/ml_popover/types'; -import { getIndexPatterns } from './api/api'; import * as i18n from './translations'; +import { IndexPatternSavedObject } from './types'; +import { getIndexPatterns } from './api/api'; type Return = [boolean, IndexPatternSavedObject[]]; @@ -22,12 +22,11 @@ export const useIndexPatterns = (refreshToggle = false): Return => { useEffect(() => { let isSubscribed = true; - const abortCtrl = new AbortController(); setIsLoading(true); async function fetchIndexPatterns() { try { - const data = await getIndexPatterns(abortCtrl.signal); + const data = await getIndexPatterns(); if (isSubscribed) { setIndexPatterns(data); @@ -44,7 +43,6 @@ export const useIndexPatterns = (refreshToggle = false): Return => { fetchIndexPatterns(); return () => { isSubscribed = false; - abortCtrl.abort(); }; }, [refreshToggle]); diff --git a/x-pack/legacy/plugins/siem/public/plugin.tsx b/x-pack/legacy/plugins/siem/public/plugin.tsx index 7911b5eb9833b4..74fc913d2b573a 100644 --- a/x-pack/legacy/plugins/siem/public/plugin.tsx +++ b/x-pack/legacy/plugins/siem/public/plugin.tsx @@ -41,6 +41,7 @@ export type Start = ReturnType; export class Plugin implements IPlugin { public id = 'siem'; public name = 'SIEM'; + constructor( // @ts-ignore this is added to satisfy the New Platform typing constraint, // but we're not leveraging any of its functionality yet. From 4f4d3d753c0fef8df7e4ac242d5f15609af243b8 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 3 Feb 2020 11:19:41 -0500 Subject: [PATCH 47/62] [Lens] Fix bugs in Lens filters (#56441) * [Lens] Fix bug where filters were not displayed * Fix #55603 Co-authored-by: Elastic Machine --- .../lens/public/app_plugin/app.test.tsx | 24 +++++++++++++++---- .../plugins/lens/public/app_plugin/app.tsx | 7 +++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index 80a7ceb61c324c..99926c646da223 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -60,10 +60,14 @@ function createMockFilterManager() { return unsubscribe; }, }), - setFilters: (newFilters: unknown[]) => { + setFilters: jest.fn((newFilters: unknown[]) => { filters = newFilters; - subscriber(); - }, + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), getFilters: () => filters, getGlobalFilters: () => { // @ts-ignore @@ -189,6 +193,13 @@ describe('Lens App', () => { `); }); + it('clears app filters on load', () => { + const defaultArgs = makeDefaultArgs(); + mount(); + + expect(defaultArgs.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); + }); + it('sets breadcrumbs when the document title changes', async () => { const defaultArgs = makeDefaultArgs(); const instance = mount(); @@ -226,7 +237,7 @@ describe('Lens App', () => { expect(args.docStorage.load).not.toHaveBeenCalled(); }); - it('loads a document and uses query if there is a document id', async () => { + it('loads a document and uses query and filters if there is a document id', async () => { const args = makeDefaultArgs(); args.editorFrame = frame; (args.docStorage.load as jest.Mock).mockResolvedValue({ @@ -234,6 +245,7 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, }); @@ -245,6 +257,9 @@ describe('Lens App', () => { expect(args.docStorage.load).toHaveBeenCalledWith('1234'); expect(args.data.indexPatterns.get).toHaveBeenCalledWith('1'); + expect(args.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ + { query: { match_phrase: { src: 'test' } } }, + ]); expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: 'fake query', @@ -260,6 +275,7 @@ describe('Lens App', () => { expression: 'valid expression', state: { query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], datasourceMetaData: { filterableIndexPatterns: [{ id: '1', title: 'saved' }] }, }, }, diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 35e45af6a3d689..6d2ebee1d88dbe 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -83,6 +83,10 @@ export function App({ const { lastKnownDoc } = state; useEffect(() => { + // Clear app-specific filters when navigating to Lens. Necessary because Lens + // can be loaded without a full page refresh + data.query.filterManager.setAppFilters([]); + const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ next: () => { setState(s => ({ ...s, filters: data.query.filterManager.getFilters() })); @@ -123,13 +127,14 @@ export function App({ core.notifications ) .then(indexPatterns => { + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters(doc.state.filters); setState(s => ({ ...s, isLoading: false, persistedDoc: doc, lastKnownDoc: doc, query: doc.state.query, - filters: doc.state.filters, indexPatternsForTopNav: indexPatterns, })); }) From 479223b0a1acf4926a18f61e2c62c458019f747d Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Mon, 3 Feb 2020 19:29:59 +0200 Subject: [PATCH 48/62] Update plugin generator to generate NP plugins (#55281) * Generate NP plugin * Added tsconfig * tsconfig * Adjust sao test * Add server side to plugin gen * Added navigation * add empty element * eslint * platform team CR * design CR improvements * text updates * temp disable plugin gen tests * eslint * Code review fixes * Add scss support - requires #53976 to be merged to work * CR fixes * comment fixes * Don't generate eslint for internal plugins by default * Update tests * reenable jest test for sao * Fix regex * review comments * code review Co-authored-by: Elastic Machine --- packages/kbn-plugin-generator/index.js | 16 ++- packages/kbn-plugin-generator/index.js.d.ts | 24 ++++ .../integration_tests/generate_plugin.test.js | 18 +-- .../kbn-plugin-generator/sao_template/sao.js | 69 +++++----- .../sao_template/sao.test.js | 127 ++++------------- .../sao_template/template/.i18nrc.json | 9 -- .../template/.kibana-plugin-helpers.json | 3 - .../sao_template/template/README.md | 31 +---- .../sao_template/template/common/index.ts | 2 + .../sao_template/template/eslintrc.js | 31 ++--- .../sao_template/template/gitignore | 6 - .../sao_template/template/index.js | 89 ------------ .../sao_template/template/kibana.json | 8 ++ .../template/package_template.json | 41 ------ .../template/public/__tests__/index.js | 7 - .../sao_template/template/public/app.js | 45 ------ .../template/public/application.tsx | 25 ++++ .../template/public/components/app.tsx | 129 ++++++++++++++++++ .../template/public/components/main/index.js | 1 - .../template/public/components/main/main.js | 97 ------------- .../sao_template/template/public/hack.js | 7 - .../template/public/{app.scss => index.scss} | 0 .../sao_template/template/public/index.ts | 16 +++ .../sao_template/template/public/plugin.ts | 42 ++++++ .../sao_template/template/public/types.ts | 11 ++ .../template/server/__tests__/index.js | 7 - .../sao_template/template/server/index.ts | 15 ++ .../sao_template/template/server/plugin.ts | 30 ++++ .../template/server/routes/example.js | 11 -- .../template/server/routes/index.ts | 17 +++ .../sao_template/template/server/types.ts | 4 + .../template/translations/zh-CN.json | 84 ------------ packages/kbn-plugin-generator/tsconfig.json | 5 + 33 files changed, 412 insertions(+), 615 deletions(-) create mode 100644 packages/kbn-plugin-generator/index.js.d.ts delete mode 100644 packages/kbn-plugin-generator/sao_template/template/.i18nrc.json delete mode 100644 packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json create mode 100644 packages/kbn-plugin-generator/sao_template/template/common/index.ts mode change 100755 => 100644 packages/kbn-plugin-generator/sao_template/template/eslintrc.js delete mode 100755 packages/kbn-plugin-generator/sao_template/template/gitignore delete mode 100755 packages/kbn-plugin-generator/sao_template/template/index.js create mode 100644 packages/kbn-plugin-generator/sao_template/template/kibana.json delete mode 100644 packages/kbn-plugin-generator/sao_template/template/package_template.json delete mode 100755 packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js delete mode 100755 packages/kbn-plugin-generator/sao_template/template/public/app.js create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/application.tsx create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx delete mode 100644 packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js delete mode 100644 packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js delete mode 100755 packages/kbn-plugin-generator/sao_template/template/public/hack.js rename packages/kbn-plugin-generator/sao_template/template/public/{app.scss => index.scss} (100%) create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/index.ts create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/plugin.ts create mode 100644 packages/kbn-plugin-generator/sao_template/template/public/types.ts delete mode 100755 packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js create mode 100644 packages/kbn-plugin-generator/sao_template/template/server/index.ts create mode 100644 packages/kbn-plugin-generator/sao_template/template/server/plugin.ts delete mode 100755 packages/kbn-plugin-generator/sao_template/template/server/routes/example.js create mode 100644 packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts create mode 100644 packages/kbn-plugin-generator/sao_template/template/server/types.ts delete mode 100644 packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json create mode 100644 packages/kbn-plugin-generator/tsconfig.json diff --git a/packages/kbn-plugin-generator/index.js b/packages/kbn-plugin-generator/index.js index 90274288357b82..15adce7f01c8e5 100644 --- a/packages/kbn-plugin-generator/index.js +++ b/packages/kbn-plugin-generator/index.js @@ -29,6 +29,7 @@ exports.run = function run(argv) { const options = getopts(argv, { alias: { h: 'help', + i: 'internal', }, }); @@ -40,17 +41,22 @@ exports.run = function run(argv) { if (options.help) { console.log( dedent(chalk` - {dim usage:} node scripts/generate-plugin {bold [name]} - - generate a fresh Kibana plugin in the plugins/ directory + # {dim Usage:} + node scripts/generate-plugin {bold [name]} + Generate a fresh Kibana plugin in the plugins/ directory + + # {dim Core Kibana plugins:} + node scripts/generate-plugin {bold [name]} -i + To generate a core Kibana plugin inside the src/plugins/ directory, add the -i flag. `) + '\n' ); process.exit(1); } const name = options._[0]; + const isKibanaPlugin = options.internal; const template = resolve(__dirname, './sao_template'); - const kibanaPlugins = resolve(__dirname, '../../plugins'); + const kibanaPlugins = resolve(__dirname, isKibanaPlugin ? '../../src/plugins' : '../../plugins'); const targetPath = resolve(kibanaPlugins, snakeCase(name)); sao({ @@ -58,6 +64,8 @@ exports.run = function run(argv) { targetPath: targetPath, configOptions: { name, + isKibanaPlugin, + targetPath, }, }).catch(error => { console.error(chalk`{red fatal error}!`); diff --git a/packages/kbn-plugin-generator/index.js.d.ts b/packages/kbn-plugin-generator/index.js.d.ts new file mode 100644 index 00000000000000..46f7c43fd57907 --- /dev/null +++ b/packages/kbn-plugin-generator/index.js.d.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +interface PluginGenerator { + /** + * Run plugin generator. + */ + run: (...args: any[]) => any; +} diff --git a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js index aa6611f3b6738d..129125c4583d52 100644 --- a/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js +++ b/packages/kbn-plugin-generator/integration_tests/generate_plugin.test.js @@ -61,7 +61,8 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug expect(stats.isDirectory()).toBe(true); }); - it(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => { + // skipped until internationalization is re-introduced + it.skip(`should create an internationalization config file with a blank line appended to satisfy the parser`, async () => { // Link to the error that happens when the blank line is not there: // https://github.com/elastic/kibana/pull/45044#issuecomment-530092627 const intlFile = `${generatedPath}/.i18nrc.json`; @@ -78,16 +79,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug }); }); - it(`'yarn test:server' should exit 0`, async () => { - await execa('yarn', ['test:server'], { - cwd: generatedPath, - env: { - DISABLE_JUNIT_REPORTER: '1', - }, - }); - }); - - it(`'yarn build' should exit 0`, async () => { + it.skip(`'yarn build' should exit 0`, async () => { await execa('yarn', ['build'], { cwd: generatedPath }); }); @@ -109,7 +101,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug '--migrations.skip=true', ], cwd: generatedPath, - wait: /ispec_plugin.+Status changed from uninitialized to green - Ready/, + wait: new RegExp('\\[ispecPlugin\\]\\[plugins\\] Setting up plugin'), }); await proc.stop('kibana'); }); @@ -120,7 +112,7 @@ describe(`running the plugin-generator via 'node scripts/generate_plugin.js plug await execa('yarn', ['preinstall'], { cwd: generatedPath }); }); - it(`'yarn lint' should exit 0`, async () => { + it.skip(`'yarn lint' should exit 0`, async () => { await execa('yarn', ['lint'], { cwd: generatedPath }); }); diff --git a/packages/kbn-plugin-generator/sao_template/sao.js b/packages/kbn-plugin-generator/sao_template/sao.js index f7401cba843588..aed4b9a02838f5 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.js +++ b/packages/kbn-plugin-generator/sao_template/sao.js @@ -17,21 +17,19 @@ * under the License. */ -const { resolve, relative, dirname } = require('path'); +const { relative } = require('path'); const startCase = require('lodash.startcase'); const camelCase = require('lodash.camelcase'); const snakeCase = require('lodash.snakecase'); -const execa = require('execa'); const chalk = require('chalk'); +const execa = require('execa'); const pkg = require('../package.json'); const kibanaPkgPath = require.resolve('../../../package.json'); const kibanaPkg = require(kibanaPkgPath); // eslint-disable-line import/no-dynamic-require -const KBN_DIR = dirname(kibanaPkgPath); - -module.exports = function({ name }) { +module.exports = function({ name, targetPath, isKibanaPlugin }) { return { prompts: { description: { @@ -47,41 +45,38 @@ module.exports = function({ name }) { message: 'Should an app component be generated?', default: true, }, - generateTranslations: { - type: 'confirm', - message: 'Should translation files be generated?', - default: true, - }, - generateHack: { - type: 'confirm', - message: 'Should a hack component be generated?', - default: true, - }, generateApi: { type: 'confirm', message: 'Should a server API be generated?', default: true, }, + // generateTranslations: { + // type: 'confirm', + // message: 'Should translation files be generated?', + // default: true, + // }, generateScss: { type: 'confirm', message: 'Should SCSS be used?', when: answers => answers.generateApp, default: true, }, + generateEslint: { + type: 'confirm', + message: 'Would you like to use a custom eslint file?', + default: !isKibanaPlugin, + }, }, filters: { + 'public/**/index.scss': 'generateScss', 'public/**/*': 'generateApp', - 'translations/**/*': 'generateTranslations', - '.i18nrc.json': 'generateTranslations', - 'public/hack.js': 'generateHack', 'server/**/*': 'generateApi', - 'public/app.scss': 'generateScss', - '.kibana-plugin-helpers.json': 'generateScss', + // 'translations/**/*': 'generateTranslations', + // '.i18nrc.json': 'generateTranslations', + 'eslintrc.js': 'generateEslint', }, move: { - gitignore: '.gitignore', 'eslintrc.js': '.eslintrc.js', - 'package_template.json': 'package.json', }, data: answers => Object.assign( @@ -91,34 +86,36 @@ module.exports = function({ name }) { camelCase, snakeCase, name, + isKibanaPlugin, + kbnVersion: answers.kbnVersion, + upperCamelCaseName: name.charAt(0).toUpperCase() + camelCase(name).slice(1), + hasUi: !!answers.generateApp, + hasServer: !!answers.generateApi, + hasScss: !!answers.generateScss, + relRoot: isKibanaPlugin ? '../../../..' : '../../..', }, answers ), enforceNewFolder: true, installDependencies: false, - gitInit: true, + gitInit: !isKibanaPlugin, async post({ log }) { - await execa('yarn', ['kbn', 'bootstrap'], { - cwd: KBN_DIR, - stdio: 'inherit', - }); - - const dir = relative(process.cwd(), resolve(KBN_DIR, 'plugins', snakeCase(name))); + const dir = relative(process.cwd(), targetPath); + // Apply eslint to the generated plugin try { - await execa('yarn', ['lint', '--fix'], { - cwd: dir, - all: true, - }); + await execa('yarn', ['lint:es', `./${dir}/**/*.ts*`, '--no-ignore', '--fix']); } catch (error) { - throw new Error(`Failure when running prettier on the generated output: ${error.all}`); + console.error(error); + throw new Error( + `Failure when running prettier on the generated output: ${error.all || error}` + ); } log.success(chalk`🎉 -Your plugin has been created in {bold ${dir}}. Move into that directory to run it: +Your plugin has been created in {bold ${dir}}. - {bold cd "${dir}"} {bold yarn start} `); }, diff --git a/packages/kbn-plugin-generator/sao_template/sao.test.js b/packages/kbn-plugin-generator/sao_template/sao.test.js index 80149c008dad84..0dbdb7d3c097b8 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.test.js +++ b/packages/kbn-plugin-generator/sao_template/sao.test.js @@ -19,8 +19,6 @@ const sao = require('sao'); -const templatePkg = require('../package.json'); - const template = { fromPath: __dirname, configOptions: { @@ -32,121 +30,57 @@ function getFileContents(file) { return file.contents.toString(); } -function getConfig(file) { - const contents = getFileContents(file).replace(/\r?\n/gm, ''); - return contents.split('kibana.Plugin(')[1]; -} - describe('plugin generator sao integration', () => { test('skips files when answering no', async () => { const res = await sao.mockPrompt(template, { generateApp: false, - generateHack: false, generateApi: false, }); - expect(res.fileList).not.toContain('public/app.js'); - expect(res.fileList).not.toContain('public/__tests__/index.js'); - expect(res.fileList).not.toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).not.toContain('app:'); - expect(uiExports).not.toContain('hacks:'); - expect(uiExports).not.toContain('init(server, options)'); - expect(uiExports).not.toContain('registerFeature('); + expect(res.fileList).toContain('common/index.ts'); + expect(res.fileList).not.toContain('public/index.ts'); + expect(res.fileList).not.toContain('server/index.ts'); }); it('includes app when answering yes', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: false, - generateApi: false, - }); - - // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).not.toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); - expect(uiExports).not.toContain('hacks:'); - }); - - it('includes hack when answering yes', async () => { - const res = await sao.mockPrompt(template, { - generateApp: true, - generateHack: true, generateApi: false, }); // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).toContain('public/hack.js'); - expect(res.fileList).not.toContain('server/routes/example.js'); - expect(res.fileList).not.toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('hacks:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); + expect(res.fileList).toContain('common/index.ts'); + expect(res.fileList).toContain('public/index.ts'); + expect(res.fileList).toContain('public/plugin.ts'); + expect(res.fileList).toContain('public/types.ts'); + expect(res.fileList).toContain('public/components/app.tsx'); + expect(res.fileList).not.toContain('server/index.ts'); }); it('includes server api when answering yes', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); // check output files - expect(res.fileList).toContain('public/app.js'); - expect(res.fileList).toContain('public/__tests__/index.js'); - expect(res.fileList).toContain('public/hack.js'); - expect(res.fileList).toContain('server/routes/example.js'); - expect(res.fileList).toContain('server/__tests__/index.js'); - - const uiExports = getConfig(res.files['index.js']); - expect(uiExports).toContain('app:'); - expect(uiExports).toContain('hacks:'); - expect(uiExports).toContain('init(server, options)'); - expect(uiExports).toContain('registerFeature('); - }); - - it('plugin config has correct name and main path', async () => { - const res = await sao.mockPrompt(template, { - generateApp: true, - generateHack: true, - generateApi: true, - }); - - const indexContents = getFileContents(res.files['index.js']); - const nameLine = indexContents.match('name: (.*)')[1]; - const mainLine = indexContents.match('main: (.*)')[1]; - - expect(nameLine).toContain('some_fancy_plugin'); - expect(mainLine).toContain('plugins/some_fancy_plugin/app'); + expect(res.fileList).toContain('public/plugin.ts'); + expect(res.fileList).toContain('server/plugin.ts'); + expect(res.fileList).toContain('server/index.ts'); + expect(res.fileList).toContain('server/types.ts'); + expect(res.fileList).toContain('server/routes/index.ts'); }); - it('plugin package has correct name', async () => { + it('plugin package has correct title', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); - const packageContents = getFileContents(res.files['package.json']); - const pkg = JSON.parse(packageContents); + const contents = getFileContents(res.files['common/index.ts']); + const controllerLine = contents.match("PLUGIN_NAME = '(.*)'")[1]; - expect(pkg.name).toBe('some_fancy_plugin'); + expect(controllerLine).toContain('Some fancy plugin'); }); it('package has version "kibana" with master', async () => { @@ -154,10 +88,10 @@ describe('plugin generator sao integration', () => { kbnVersion: 'master', }); - const packageContents = getFileContents(res.files['package.json']); + const packageContents = getFileContents(res.files['kibana.json']); const pkg = JSON.parse(packageContents); - expect(pkg.kibana.version).toBe('kibana'); + expect(pkg.version).toBe('master'); }); it('package has correct version', async () => { @@ -165,39 +99,26 @@ describe('plugin generator sao integration', () => { kbnVersion: 'v6.0.0', }); - const packageContents = getFileContents(res.files['package.json']); - const pkg = JSON.parse(packageContents); - - expect(pkg.kibana.version).toBe('v6.0.0'); - }); - - it('package has correct templateVersion', async () => { - const res = await sao.mockPrompt(template, { - kbnVersion: 'master', - }); - - const packageContents = getFileContents(res.files['package.json']); + const packageContents = getFileContents(res.files['kibana.json']); const pkg = JSON.parse(packageContents); - expect(pkg.kibana.templateVersion).toBe(templatePkg.version); + expect(pkg.version).toBe('v6.0.0'); }); it('sample app has correct values', async () => { const res = await sao.mockPrompt(template, { generateApp: true, - generateHack: true, generateApi: true, }); - const contents = getFileContents(res.files['public/app.js']); - const controllerLine = contents.match('setRootController(.*)')[1]; + const contents = getFileContents(res.files['common/index.ts']); + const controllerLine = contents.match("PLUGIN_ID = '(.*)'")[1]; expect(controllerLine).toContain('someFancyPlugin'); }); it('includes dotfiles', async () => { const res = await sao.mockPrompt(template); - expect(res.files['.gitignore']).toBeTruthy(); expect(res.files['.eslintrc.js']).toBeTruthy(); }); }); diff --git a/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json b/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json deleted file mode 100644 index 1a8aea88538768..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/.i18nrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "paths": { - "<%= camelCase(name) %>": "./" - }, - "translations": [ - "translations/zh-CN.json" - ] -} - diff --git a/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json b/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json deleted file mode 100644 index 383368c7f8ce1d..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/.kibana-plugin-helpers.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "styleSheetToCompile": "public/app.scss" -} diff --git a/packages/kbn-plugin-generator/sao_template/template/README.md b/packages/kbn-plugin-generator/sao_template/template/README.md index 59c3adf2713c8d..1e0139428fcbc4 100755 --- a/packages/kbn-plugin-generator/sao_template/template/README.md +++ b/packages/kbn-plugin-generator/sao_template/template/README.md @@ -6,34 +6,7 @@ --- -## development +## Development -See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. Once you have completed that, use the following yarn scripts. +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. - - `yarn kbn bootstrap` - - Install dependencies and crosslink Kibana and all projects/plugins. - - > ***IMPORTANT:*** Use this script instead of `yarn` to install dependencies when switching branches, and re-run it whenever your dependencies change. - - - `yarn start` - - Start kibana and have it include this plugin. You can pass any arguments that you would normally send to `bin/kibana` - - ``` - yarn start --elasticsearch.hosts http://localhost:9220 - ``` - - - `yarn build` - - Build a distributable archive of your plugin. - - - `yarn test:browser` - - Run the browser tests in a real web browser. - - - `yarn test:mocha` - - Run the server tests using mocha. - -For more information about any of these commands run `yarn ${task} --help`. For a full list of tasks checkout the `package.json` file, or run `yarn run`. diff --git a/packages/kbn-plugin-generator/sao_template/template/common/index.ts b/packages/kbn-plugin-generator/sao_template/template/common/index.ts new file mode 100644 index 00000000000000..90ffcb70045aa5 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/common/index.ts @@ -0,0 +1,2 @@ +export const PLUGIN_ID = '<%= camelCase(name) %>'; +export const PLUGIN_NAME = '<%= name %>'; diff --git a/packages/kbn-plugin-generator/sao_template/template/eslintrc.js b/packages/kbn-plugin-generator/sao_template/template/eslintrc.js old mode 100755 new mode 100644 index e1dfadc212b7e5..b68d42e32e0472 --- a/packages/kbn-plugin-generator/sao_template/template/eslintrc.js +++ b/packages/kbn-plugin-generator/sao_template/template/eslintrc.js @@ -1,24 +1,9 @@ -module.exports = { - root: true, +module.exports = { + root: true, extends: ['@elastic/eslint-config-kibana', 'plugin:@elastic/eui/recommended'], - settings: { - 'import/resolver': { - '@kbn/eslint-import-resolver-kibana': { - rootPackageName: '<%= snakeCase(name) %>', - }, - }, - }, - overrides: [ - { - files: ['**/public/**/*'], - settings: { - 'import/resolver': { - '@kbn/eslint-import-resolver-kibana': { - forceNode: false, - rootPackageName: '<%= snakeCase(name) %>', - }, - }, - }, - }, - ] -}; + <%_ if (!isKibanaPlugin) { -%> + rules: { + "@kbn/eslint/require-license-header": "off" + } + <%_ } -%> +}; \ No newline at end of file diff --git a/packages/kbn-plugin-generator/sao_template/template/gitignore b/packages/kbn-plugin-generator/sao_template/template/gitignore deleted file mode 100755 index db28fed19376d5..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/gitignore +++ /dev/null @@ -1,6 +0,0 @@ -npm-debug.log* -node_modules -/build/ -<%_ if (generateScss) { -%> -/public/app.css -<%_ } -%> diff --git a/packages/kbn-plugin-generator/sao_template/template/index.js b/packages/kbn-plugin-generator/sao_template/template/index.js deleted file mode 100755 index 4bc3347ae60194..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/index.js +++ /dev/null @@ -1,89 +0,0 @@ -<% if (generateScss) { -%> -import { resolve } from 'path'; -import { existsSync } from 'fs'; - -<% } -%> - -<% if (generateApp) { -%> -import { i18n } from '@kbn/i18n'; -<% } -%> - -<% if (generateApi) { -%> -import exampleRoute from './server/routes/example'; - -<% } -%> -export default function (kibana) { - return new kibana.Plugin({ - require: ['elasticsearch'], - name: '<%= snakeCase(name) %>', - uiExports: { - <%_ if (generateApp) { -%> - app: { - title: '<%= startCase(name) %>', - description: '<%= description %>', - main: 'plugins/<%= snakeCase(name) %>/app', - }, - <%_ } -%> - <%_ if (generateHack) { -%> - hacks: [ - 'plugins/<%= snakeCase(name) %>/hack' - ], - <%_ } -%> - <%_ if (generateScss) { -%> - styleSheetPaths: [resolve(__dirname, 'public/app.scss'), resolve(__dirname, 'public/app.css')].find(p => existsSync(p)), - <%_ } -%> - }, - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - <%_ if (generateApi || generateApp) { -%> - - // eslint-disable-next-line no-unused-vars - init(server, options) { - <%_ if (generateApp) { -%> - const xpackMainPlugin = server.plugins.xpack_main; - if (xpackMainPlugin) { - const featureId = '<%= snakeCase(name) %>'; - - xpackMainPlugin.registerFeature({ - id: featureId, - name: i18n.translate('<%= camelCase(name) %>.featureRegistry.featureName', { - defaultMessage: '<%= name %>', - }), - navLinkId: featureId, - icon: 'questionInCircle', - app: [featureId, 'kibana'], - catalogue: [], - privileges: { - all: { - api: [], - savedObject: { - all: [], - read: [], - }, - ui: ['show'], - }, - read: { - api: [], - savedObject: { - all: [], - read: [], - }, - ui: ['show'], - }, - }, - }); - } - <%_ } -%> - - <%_ if (generateApi) { -%> - // Add server routes and initialize the plugin here - exampleRoute(server); - <%_ } -%> - } - <%_ } -%> - }); -} diff --git a/packages/kbn-plugin-generator/sao_template/template/kibana.json b/packages/kbn-plugin-generator/sao_template/template/kibana.json new file mode 100644 index 00000000000000..f8bb07040abeb6 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "<%= camelCase(name) %>", + "version": "<%= kbnVersion %>", + "server": <%= hasServer %>, + "ui": <%= hasUi %>, + "requiredPlugins": ["navigation"], + "optionalPlugins": [] +} diff --git a/packages/kbn-plugin-generator/sao_template/template/package_template.json b/packages/kbn-plugin-generator/sao_template/template/package_template.json deleted file mode 100644 index 4b6629fa902685..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/package_template.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "<%= snakeCase(name) %>", - "version": "0.0.0", - "description": "<%= description %>", - "main": "index.js", - "kibana": { - "version": "<%= (kbnVersion === 'master') ? 'kibana' : kbnVersion %>", - "templateVersion": "<%= templateVersion %>" - }, - "scripts": { - "preinstall": "node ../../preinstall_check", - "kbn": "node ../../scripts/kbn", - "es": "node ../../scripts/es", - "lint": "eslint .", - "start": "plugin-helpers start", - "test:server": "plugin-helpers test:server", - "test:browser": "plugin-helpers test:browser", - "build": "plugin-helpers build" - }, - <%_ if (generateTranslations) { _%> - "dependencies": { - "@kbn/i18n": "link:../../packages/kbn-i18n" - }, - <%_ } _%> - "devDependencies": { - "@elastic/eslint-config-kibana": "link:../../packages/eslint-config-kibana", - "@elastic/eslint-import-resolver-kibana": "link:../../packages/kbn-eslint-import-resolver-kibana", - "@kbn/expect": "link:../../packages/kbn-expect", - "@kbn/plugin-helpers": "link:../../packages/kbn-plugin-helpers", - "babel-eslint": "^10.0.1", - "eslint": "^5.16.0", - "eslint-plugin-babel": "^5.3.0", - "eslint-plugin-import": "^2.16.0", - "eslint-plugin-jest": "^22.4.1", - "eslint-plugin-jsx-a11y": "^6.2.1", - "eslint-plugin-mocha": "^5.3.0", - "eslint-plugin-no-unsanitized": "^3.0.2", - "eslint-plugin-prefer-object-spread": "^1.2.1", - "eslint-plugin-react": "^7.12.4" - } -} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js b/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js deleted file mode 100755 index 9320bd7b028a86..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/__tests__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import expect from '@kbn/expect'; - -describe('suite', () => { - it('is a test', () => { - expect(true).to.equal(true); - }); -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/app.js b/packages/kbn-plugin-generator/sao_template/template/public/app.js deleted file mode 100755 index 37a7c37e916a0f..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/app.js +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; -import { render, unmountComponentAtNode } from 'react-dom'; -<%_ if (generateTranslations) { _%> -import { I18nProvider } from '@kbn/i18n/react'; -<%_ } _%> - -import { Main } from './components/main'; - -const app = uiModules.get('apps/<%= camelCase(name) %>'); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); -app.config(stateManagementConfigProvider => - stateManagementConfigProvider.disable() -); - -function RootController($scope, $element, $http) { - const domNode = $element[0]; - - // render react to DOM - <%_ if (generateTranslations) { _%> - render( - -
- , - domNode - ); - <%_ } else { _%> - render(
, domNode); - <%_ } _%> - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('<%= camelCase(name) %>', RootController); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/application.tsx b/packages/kbn-plugin-generator/sao_template/template/public/application.tsx new file mode 100644 index 00000000000000..8106a18a784e7b --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/application.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppMountParameters, CoreStart } from '<%= relRoot %>/src/core/public'; +import { AppPluginStartDependencies } from './types'; +import { <%= upperCamelCaseName %>App } from './components/app'; + + +export const renderApp = ( + { notifications, http }: CoreStart, + { navigation }: AppPluginStartDependencies, + { appBasePath, element }: AppMountParameters + ) => { + ReactDOM.render( + <<%= upperCamelCaseName %>App + basename={appBasePath} + notifications={notifications} + http={http} + navigation={navigation} + />, + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); + }; + \ No newline at end of file diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx new file mode 100644 index 00000000000000..7b259a9c5b99d6 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/components/app.tsx @@ -0,0 +1,129 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import { BrowserRouter as Router } from 'react-router-dom'; + +import { + EuiButton, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageHeader, + EuiTitle, + EuiText, +} from '@elastic/eui'; + +import { CoreStart } from '<%= relRoot %>/../src/core/public'; +import { NavigationPublicPluginStart } from '<%= relRoot %>/../src/plugins/navigation/public'; + +import { PLUGIN_ID, PLUGIN_NAME } from '../../common'; + +interface <%= upperCamelCaseName %>AppDeps { + basename: string; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; +} + +export const <%= upperCamelCaseName %>App = ({ basename, notifications, http, navigation }: <%= upperCamelCaseName %>AppDeps) => { + // Use React hooks to manage state. + const [timestamp, setTimestamp] = useState(); + + const onClickHandler = () => { +<%_ if (generateApi) { -%> + // Use the core http service to make a response to the server API. + http.get('/api/<%= snakeCase(name) %>/example').then(res => { + setTimestamp(res.time); + // Use the core notifications service to display a success message. + notifications.toasts.addSuccess(i18n.translate('<%= camelCase(name) %>.dataUpdated', { + defaultMessage: 'Data updated', + })); + }); +<%_ } else { -%> + setTimestamp(new Date().toISOString()); + notifications.toasts.addSuccess(PLUGIN_NAME); +<%_ } -%> + }; + + // Render the application DOM. + // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. + return ( + + + <> + + + + + +

+ +

+
+
+ + + +

+ +

+
+
+ + +

+ +

+ +

+ +

+ + + +
+
+
+
+
+ +
+
+ ); +}; diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js b/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js deleted file mode 100644 index 68710baa1bee8e..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/components/main/index.js +++ /dev/null @@ -1 +0,0 @@ -export { Main } from './main'; diff --git a/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js b/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js deleted file mode 100644 index 59fd667c709aa2..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/components/main/main.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { - EuiPage, - EuiPageHeader, - EuiTitle, - EuiPageBody, - EuiPageContent, - EuiPageContentHeader, - EuiPageContentBody, - EuiText -} from '@elastic/eui'; -<%_ if (generateTranslations) { _%> -import { FormattedMessage } from '@kbn/i18n/react'; -<%_ } _%> - -export class Main extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { - /* - FOR EXAMPLE PURPOSES ONLY. There are much better ways to - manage state and update your UI than this. - */ - const { httpClient } = this.props; - httpClient.get('../api/<%= name %>/example').then((resp) => { - this.setState({ time: resp.data.time }); - }); - } - render() { - const { title } = this.props; - return ( - - - - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - {title} Hello World! - <%_ } _%> -

-
-
- - - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - Congratulations - <%_ } _%> -

-
-
- - -

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - You have successfully created your first Kibana Plugin! - <%_ } _%> -

-

- <%_ if (generateTranslations) { _%> - - <%_ } else { _%> - The server time (via API call) is {this.state.time || 'NO API CALL YET'} - <%_ } _%> -

-
-
-
-
-
- ); - } -} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/hack.js b/packages/kbn-plugin-generator/sao_template/template/public/hack.js deleted file mode 100755 index 775526c8e44a30..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/public/hack.js +++ /dev/null @@ -1,7 +0,0 @@ -import $ from 'jquery'; - -$(document.body).on('keypress', function (event) { - if (event.which === 58) { - alert('boo!'); - } -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/public/app.scss b/packages/kbn-plugin-generator/sao_template/template/public/index.scss similarity index 100% rename from packages/kbn-plugin-generator/sao_template/template/public/app.scss rename to packages/kbn-plugin-generator/sao_template/template/public/index.scss diff --git a/packages/kbn-plugin-generator/sao_template/template/public/index.ts b/packages/kbn-plugin-generator/sao_template/template/public/index.ts new file mode 100644 index 00000000000000..2999dc7264ddb6 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/index.ts @@ -0,0 +1,16 @@ +<%_ if (hasScss) { -%> +import './index.scss'; +<%_ } -%> + +import { <%= upperCamelCaseName %>Plugin } from './plugin'; + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. +export function plugin() { + return new <%= upperCamelCaseName %>Plugin(); +} +export { + <%= upperCamelCaseName %>PluginSetup, + <%= upperCamelCaseName %>PluginStart, +} from './types'; + diff --git a/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts b/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts new file mode 100644 index 00000000000000..76f7f1a6f99082 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/plugin.ts @@ -0,0 +1,42 @@ +import { i18n } from '@kbn/i18n'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '<%= relRoot %>/src/core/public'; +import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart, AppPluginStartDependencies } from './types'; +import { PLUGIN_NAME } from '../common'; + +export class <%= upperCamelCaseName %>Plugin + implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> { + + public setup(core: CoreSetup): <%= upperCamelCaseName %>PluginSetup { + // Register an application into the side navigation menu + core.application.register({ + id: '<%= camelCase(name) %>', + title: PLUGIN_NAME, + async mount(params: AppMountParameters) { + // Load application bundle + const { renderApp } = await import('./application'); + // Get start services as specified in kibana.json + const [coreStart, depsStart] = await core.getStartServices(); + // Render the application + return renderApp(coreStart, depsStart as AppPluginStartDependencies, params); + }, + }); + + // Return methods that should be available to other plugins + return { + getGreeting() { + return i18n.translate('<%= camelCase(name) %>.greetingText', { + defaultMessage: 'Hello from {name}!', + values: { + name: PLUGIN_NAME, + }, + }); + }, + }; + } + + public start(core: CoreStart): <%= upperCamelCaseName %>PluginStart { + return {}; + } + + public stop() {} +} diff --git a/packages/kbn-plugin-generator/sao_template/template/public/types.ts b/packages/kbn-plugin-generator/sao_template/template/public/types.ts new file mode 100644 index 00000000000000..2ebb0c0d1257f5 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/public/types.ts @@ -0,0 +1,11 @@ +import { NavigationPublicPluginStart } from '<%= relRoot %>/src/plugins/navigation/public'; + +export interface <%= upperCamelCaseName %>PluginSetup { + getGreeting: () => string; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginStart {} + +export interface AppPluginStartDependencies { + navigation: NavigationPublicPluginStart +}; diff --git a/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js b/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js deleted file mode 100755 index 9320bd7b028a86..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/server/__tests__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import expect from '@kbn/expect'; - -describe('suite', () => { - it('is a test', () => { - expect(true).to.equal(true); - }); -}); diff --git a/packages/kbn-plugin-generator/sao_template/template/server/index.ts b/packages/kbn-plugin-generator/sao_template/template/server/index.ts new file mode 100644 index 00000000000000..816b8faec2a45b --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/index.ts @@ -0,0 +1,15 @@ +import { PluginInitializerContext } from '<%= relRoot %>/src/core/server'; +import { <%= upperCamelCaseName %>Plugin } from './plugin'; + + +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + + export function plugin(initializerContext: PluginInitializerContext) { + return new <%= upperCamelCaseName %>Plugin(initializerContext); +} + +export { + <%= upperCamelCaseName %>PluginSetup, + <%= upperCamelCaseName %>PluginStart, +} from './types'; diff --git a/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts b/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts new file mode 100644 index 00000000000000..d6a343209e39ec --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/plugin.ts @@ -0,0 +1,30 @@ +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from '<%= relRoot %>/src/core/server'; + +import { <%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart } from './types'; +import { defineRoutes } from './routes'; + +export class <%= upperCamelCaseName %>Plugin + implements Plugin<<%= upperCamelCaseName %>PluginSetup, <%= upperCamelCaseName %>PluginStart> { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('<%= name %>: Setup'); + const router = core.http.createRouter(); + + // Register server side APIs + defineRoutes(router); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('<%= name %>: Started'); + return {}; + } + + public stop() {} +} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js b/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js deleted file mode 100755 index 5a612645f48fc3..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/server/routes/example.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function (server) { - - server.route({ - path: '/api/<%= name %>/example', - method: 'GET', - handler() { - return { time: (new Date()).toISOString() }; - } - }); - -} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts b/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts new file mode 100644 index 00000000000000..d8bb00f0dea6c5 --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/routes/index.ts @@ -0,0 +1,17 @@ +import { IRouter } from '<%= relRoot %>/../src/core/server'; + +export function defineRoutes(router: IRouter) { + router.get( + { + path: '/api/<%= snakeCase(name) %>/example', + validate: false, + }, + async (context, request, response) => { + return response.ok({ + body: { + time: new Date().toISOString(), + }, + }); + } + ); +} diff --git a/packages/kbn-plugin-generator/sao_template/template/server/types.ts b/packages/kbn-plugin-generator/sao_template/template/server/types.ts new file mode 100644 index 00000000000000..adbc5e93f03c5f --- /dev/null +++ b/packages/kbn-plugin-generator/sao_template/template/server/types.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface <%= upperCamelCaseName %>PluginStart {} diff --git a/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json b/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json deleted file mode 100644 index 3447511c6739a1..00000000000000 --- a/packages/kbn-plugin-generator/sao_template/template/translations/zh-CN.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "formats": { - "number": { - "currency": { - "style": "currency" - }, - "percent": { - "style": "percent" - } - }, - "date": { - "short": { - "month": "numeric", - "day": "numeric", - "year": "2-digit" - }, - "medium": { - "month": "short", - "day": "numeric", - "year": "numeric" - }, - "long": { - "month": "long", - "day": "numeric", - "year": "numeric" - }, - "full": { - "weekday": "long", - "month": "long", - "day": "numeric", - "year": "numeric" - } - }, - "time": { - "short": { - "hour": "numeric", - "minute": "numeric" - }, - "medium": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric" - }, - "long": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short" - }, - "full": { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short" - } - }, - "relative": { - "years": { - "units": "year" - }, - "months": { - "units": "month" - }, - "days": { - "units": "day" - }, - "hours": { - "units": "hour" - }, - "minutes": { - "units": "minute" - }, - "seconds": { - "units": "second" - } - } - }, - "messages": { - "<%= camelCase(name) %>.congratulationsText": "您已经成功创建第一个 Kibana 插件。", - "<%= camelCase(name) %>.congratulationsTitle": "恭喜!", - "<%= camelCase(name) %>.helloWorldText": "{title} 您好,世界!", - "<%= camelCase(name) %>.serverTimeText": "服务器时间(通过 API 调用)为 {time}" - } -} diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json new file mode 100644 index 00000000000000..fe0f7112f1fa9e --- /dev/null +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["**/*", "index.js.d.ts"], + "exclude": ["sao_template/template/*"] +} From 4aa727560a72b3f284910dd34e78d5b9dbc81ebc Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 3 Feb 2020 19:00:40 +0100 Subject: [PATCH 49/62] fix timespan referencing to same values (#56601) (#56612) --- .../server/lib/adapters/monitor_states/search/query_context.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts index 961cc94dcea19f..a51931ba11630b 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitor_states/search/query_context.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { APICaller } from 'kibana/server'; import { CursorPagination } from '../adapter_types'; import { INDEX_NAMES } from '../../../../../common/constants'; @@ -97,7 +98,7 @@ export class QueryContext { // behavior. const tsEnd = parseRelativeDate(this.dateRangeEnd, { roundUp: true })!; - const tsStart = tsEnd.subtract(5, 'minutes'); + const tsStart = moment(tsEnd).subtract(5, 'minutes'); return { range: { From e28e149b46d27df6f4477a4e83600742431873b6 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 3 Feb 2020 11:20:41 -0800 Subject: [PATCH 50/62] Fix incorrect app name in ILM license checker. (#56355) --- .../server/lib/check_license/check_license.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js index e71820a346f918..7534f3cd0934ee 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/server/lib/check_license/check_license.js @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; export function checkLicense(xpackLicenseInfo) { - const pluginName = 'Index Management'; + const pluginName = 'Index Lifecycle Policies'; // If, for some reason, we cannot get the license information // from Elasticsearch, assume worst case and disable From 6398a9911df0d45ba72dafbb2d9c8398916ddba6 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 3 Feb 2020 15:55:50 -0500 Subject: [PATCH 51/62] [Monitoring] Migrate license expiration alert to Kibana alerting (#54306) * License expiration * Flip off * Only require alerting and actions if enabled * Support date formating and timezones in the alert UI messages, support ccs better * Fix status tests * Fix up front end tests * Fix linting, and switch this back * Add this back in so legacy alerts continue to work * Fix type issues * Handle CCS better * Code cleanup * Fix type issues * Flip this off, and fix test * Moved the email address config to advanced settings, but need help with test failures and typescript * Fix issue with task manager * Deprecate email_address * Use any until we can figure out this TS issue * Fix type issue * More tests * Fix mocha tests * Use mock instead of any * I'm not sure why these changed... * Provide timezone in moment usage in tests for consistency * Fix type issue * Change how we get dateFormat and timezone * Change where we calculate the dates to show in the alerts UI * Show deprecation warning based on the feature toggle * Ensure we are using UTC * PR feedback * Only add this if the feature flag is enabled * Fix tests * Ensure we only attempt to look this up if the feature flag is enabled Co-authored-by: Elastic Machine --- .../common/{constants.js => constants.ts} | 42 ++ .../legacy/plugins/monitoring/deprecations.js | 20 +- x-pack/legacy/plugins/monitoring/index.js | 12 +- .../alerts/__snapshots__/status.test.tsx.snap | 70 +++ .../__snapshots__/configuration.test.tsx.snap | 120 +++++ .../__snapshots__/step1.test.tsx.snap | 297 ++++++++++++ .../__snapshots__/step2.test.tsx.snap | 49 ++ .../__snapshots__/step3.test.tsx.snap | 95 ++++ .../configuration/configuration.test.tsx | 147 ++++++ .../alerts/configuration/configuration.tsx | 193 ++++++++ .../components/alerts/configuration/index.ts | 7 + .../alerts/configuration/step1.test.tsx | 338 +++++++++++++ .../components/alerts/configuration/step1.tsx | 334 +++++++++++++ .../alerts/configuration/step2.test.tsx | 51 ++ .../components/alerts/configuration/step2.tsx | 38 ++ .../alerts/configuration/step3.test.tsx | 48 ++ .../components/alerts/configuration/step3.tsx | 47 ++ .../components/alerts/manage_email_action.tsx | 301 ++++++++++++ .../public/components/alerts/status.test.tsx | 81 ++++ .../public/components/alerts/status.tsx | 203 ++++++++ .../cluster/overview/alerts_panel.js | 62 ++- .../components/cluster/overview/index.js | 14 +- .../plugins/monitoring/public/jest.helpers.ts | 36 ++ ...rror_handler.js => ajax_error_handler.tsx} | 7 +- .../monitoring/public/lib/form_validation.ts | 48 ++ .../monitoring/public/lib/setup_mode.test.js | 8 +- .../lib/{setup_mode.js => setup_mode.tsx} | 62 ++- .../monitoring/public/views/alerts/index.js | 2 +- .../public/views/cluster/overview/index.js | 21 +- .../server/alerts/license_expiration.test.ts | 453 ++++++++++++++++++ .../server/alerts/license_expiration.ts | 162 +++++++ .../monitoring/server/alerts/types.d.ts | 45 ++ .../lib/alerts/fetch_available_ccs.test.ts | 36 ++ .../server/lib/alerts/fetch_available_ccs.ts | 19 + .../server/lib/alerts/fetch_clusters.test.ts | 33 ++ .../server/lib/alerts/fetch_clusters.ts | 52 ++ .../fetch_default_email_address.test.ts | 17 + .../lib/alerts/fetch_default_email_address.ts | 13 + .../server/lib/alerts/fetch_licenses.test.ts | 105 ++++ .../server/lib/alerts/fetch_licenses.ts | 67 +++ .../server/lib/alerts/fetch_status.ts | 87 ++++ .../lib/alerts/get_ccs_index_pattern.test.ts | 24 + .../lib/alerts/get_ccs_index_pattern.ts | 13 + .../lib/alerts/license_expiration.lib.test.ts | 55 +++ .../lib/alerts/license_expiration.lib.ts | 58 +++ .../lib/cluster/get_clusters_from_request.js | 32 +- .../monitoring/server/lib/get_date_format.js | 9 + .../setup/collection/get_collection_status.js | 1 - .../plugins/monitoring/server/plugin.js | 39 +- .../server/routes/api/v1/alerts/alerts.js | 89 ++++ .../server/routes/api/v1/alerts/index.js | 53 +- .../routes/api/v1/alerts/legacy_alerts.js | 57 +++ .../monitoring/server/routes/api/v1/ui.js | 2 +- .../legacy/plugins/monitoring/ui_exports.js | 72 ++- x-pack/plugins/actions/common/types.ts | 7 + 55 files changed, 4223 insertions(+), 130 deletions(-) rename x-pack/legacy/plugins/monitoring/common/{constants.js => constants.ts} (85%) create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx create mode 100644 x-pack/legacy/plugins/monitoring/public/jest.helpers.ts rename x-pack/legacy/plugins/monitoring/public/lib/{ajax_error_handler.js => ajax_error_handler.tsx} (94%) create mode 100644 x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts rename x-pack/legacy/plugins/monitoring/public/lib/{setup_mode.js => setup_mode.tsx} (76%) create mode 100644 x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts create mode 100644 x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js create mode 100644 x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js create mode 100644 x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js diff --git a/x-pack/legacy/plugins/monitoring/common/constants.js b/x-pack/legacy/plugins/monitoring/common/constants.ts similarity index 85% rename from x-pack/legacy/plugins/monitoring/common/constants.js rename to x-pack/legacy/plugins/monitoring/common/constants.ts index ff16b0e9c5167f..53764f592dc151 100644 --- a/x-pack/legacy/plugins/monitoring/common/constants.js +++ b/x-pack/legacy/plugins/monitoring/common/constants.ts @@ -233,3 +233,45 @@ export const REPORTING_SYSTEM_ID = 'reporting'; * @type {Number} */ export const TELEMETRY_COLLECTION_INTERVAL = 86400000; + +/** + * We want to slowly rollout the migration from watcher-based cluster alerts to + * kibana alerts and we only want to enable the kibana alerts once all + * watcher-based cluster alerts have been migrated so this flag will serve + * as the only way to see the new UI and actually run Kibana alerts. It will + * be false until all alerts have been migrated, then it will be removed + */ +export const KIBANA_ALERTING_ENABLED = false; + +/** + * The prefix for all alert types used by monitoring + */ +export const ALERT_TYPE_PREFIX = 'monitoring_'; + +/** + * This is the alert type id for the license expiration alert + */ +export const ALERT_TYPE_LICENSE_EXPIRATION = `${ALERT_TYPE_PREFIX}alert_type_license_expiration`; + +/** + * A listing of all alert types + */ +export const ALERT_TYPES = [ALERT_TYPE_LICENSE_EXPIRATION]; + +/** + * Matches the id for the built-in in email action type + * See x-pack/legacy/plugins/actions/server/builtin_action_types/email.ts + */ +export const ALERT_ACTION_TYPE_EMAIL = '.email'; + +/** + * The number of alerts that have been migrated + */ +export const NUMBER_OF_MIGRATED_ALERTS = 1; + +/** + * The advanced settings config name for the email address + */ +export const MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS = 'monitoring:alertingEmailAddress'; + +export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; diff --git a/x-pack/legacy/plugins/monitoring/deprecations.js b/x-pack/legacy/plugins/monitoring/deprecations.js index 6e35e86dd9d718..ae8650fd3b26a6 100644 --- a/x-pack/legacy/plugins/monitoring/deprecations.js +++ b/x-pack/legacy/plugins/monitoring/deprecations.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from './common/constants'; +import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY, KIBANA_ALERTING_ENABLED } from './common/constants'; /** * Re-writes deprecated user-defined config settings and logs warnings as a @@ -21,10 +21,20 @@ export const deprecations = () => { const clusterAlertsEnabled = get(settings, 'cluster_alerts.enabled'); const emailNotificationsEnabled = clusterAlertsEnabled && get(settings, 'cluster_alerts.email_notifications.enabled'); - if (emailNotificationsEnabled && !get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { - log( - `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" will be required for email notifications to work in 7.0."` - ); + if (emailNotificationsEnabled) { + if (KIBANA_ALERTING_ENABLED) { + if (get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { + log( + `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" is deprecated. Please configure the email adddress through the Stack Monitoring UI instead."` + ); + } + } else { + if (!get(settings, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { + log( + `Config key "${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}" will be required for email notifications to work in 7.0."` + ); + } + } } }, (settings, log) => { diff --git a/x-pack/legacy/plugins/monitoring/index.js b/x-pack/legacy/plugins/monitoring/index.js index ca595836133c2c..ade172f527dabd 100644 --- a/x-pack/legacy/plugins/monitoring/index.js +++ b/x-pack/legacy/plugins/monitoring/index.js @@ -10,15 +10,20 @@ import { deprecations } from './deprecations'; import { getUiExports } from './ui_exports'; import { Plugin } from './server/plugin'; import { initInfraSource } from './server/lib/logs/init_infra_source'; +import { KIBANA_ALERTING_ENABLED } from './common/constants'; /** * Invokes plugin modules to instantiate the Monitoring plugin for Kibana * @param kibana {Object} Kibana plugin instance * @return {Object} Monitoring UI Kibana plugin object */ +const deps = ['kibana', 'elasticsearch', 'xpack_main']; +if (KIBANA_ALERTING_ENABLED) { + deps.push(...['alerting', 'actions']); +} export const monitoring = kibana => new kibana.Plugin({ - require: ['kibana', 'elasticsearch', 'xpack_main'], + require: deps, id: 'monitoring', configPrefix: 'monitoring', publicDir: resolve(__dirname, 'public'), @@ -59,6 +64,7 @@ export const monitoring = kibana => }), injectUiAppVars: server.injectUiAppVars, log: (...args) => server.log(...args), + logger: server.newPlatform.coreContext.logger, getOSInfo: server.getOSInfo, events: { on: (...args) => server.events.on(...args), @@ -73,11 +79,13 @@ export const monitoring = kibana => xpack_main: server.plugins.xpack_main, elasticsearch: server.plugins.elasticsearch, infra: server.plugins.infra, + alerting: server.plugins.alerting, usageCollection, licensing, }; - new Plugin().setup(serverFacade, plugins); + const plugin = new Plugin(); + plugin.setup(serverFacade, plugins); }, config, deprecations, diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap new file mode 100644 index 00000000000000..4cf1f4df2eb2e6 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/__snapshots__/status.test.tsx.snap @@ -0,0 +1,70 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Status should render a flyout when clicking the link 1`] = ` + + + +

+ Monitoring alerts +

+
+ +

+ Configure an email server and email address to receive alerts. +

+
+
+ + + +
+`; + +exports[`Status should render a success message if all alerts have been migrated and in setup mode 1`] = ` + +

+ + Want to make changes? Click here. + +

+
+`; + +exports[`Status should render without setup mode 1`] = ` + + +

+ + Migrate cluster alerts to our new alerting platform. + +

+
+ +
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap new file mode 100644 index 00000000000000..f044e001700c50 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/configuration.test.tsx.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Configuration shallow view should render step 1 1`] = ` + + + Create new email action... + , + "inputDisplay": + Create new email action... + , + "value": "__new__", + }, + ] + } + valueOfSelected="" + /> + +`; + +exports[`Configuration shallow view should render step 2 1`] = ` + + + + + +`; + +exports[`Configuration shallow view should render step 3 1`] = ` + + + Save + + +`; + +exports[`Configuration should render high level steps 1`] = ` +
+ + + + + + + + + +
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap new file mode 100644 index 00000000000000..fa03769ea3d090 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step1.test.tsx.snap @@ -0,0 +1,297 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step1 creating should render a create form 1`] = ` + + + + + +`; + +exports[`Step1 editing should allow for editing 1`] = ` + + +

+ Edit the action below. +

+
+ + +
+`; + +exports[`Step1 should render normally 1`] = ` + + + From: , Service: + , + "inputDisplay": + From: , Service: + , + "value": "1", + }, + Object { + "dropdownDisplay": + Create new email action... + , + "inputDisplay": + Create new email action... + , + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + + + + + Edit + + + + + Test + + + + + Delete + + + + +`; + +exports[`Step1 testing should should a tooltip if there is no email address 1`] = ` + + + Test + + +`; + +exports[`Step1 testing should show a failed test error 1`] = ` + + + From: , Service: + , + "inputDisplay": + From: , Service: + , + "value": "1", + }, + Object { + "dropdownDisplay": + Create new email action... + , + "inputDisplay": + Create new email action... + , + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + + + + + Edit + + + + + Test + + + + + Delete + + + + + +

+ Very detailed error message +

+
+
+`; + +exports[`Step1 testing should show a successful test 1`] = ` + + + From: , Service: + , + "inputDisplay": + From: , Service: + , + "value": "1", + }, + Object { + "dropdownDisplay": + Create new email action... + , + "inputDisplay": + Create new email action... + , + "value": "__new__", + }, + ] + } + valueOfSelected="1" + /> + + + + + Edit + + + + + Test + + + + + Delete + + + + + +

+ Looks good on our end! +

+
+
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap new file mode 100644 index 00000000000000..bac183618b4912 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step2.test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step2 should render normally 1`] = ` + + + + + +`; + +exports[`Step2 should show form errors 1`] = ` + + + + + +`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap new file mode 100644 index 00000000000000..ed15ae9a9cff7f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/__snapshots__/step3.test.tsx.snap @@ -0,0 +1,95 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Step3 should render normally 1`] = ` + + + Save + + +`; + +exports[`Step3 should show a disabled state 1`] = ` + + + Save + + +`; + +exports[`Step3 should show a saving state 1`] = ` + + + Save + + +`; + +exports[`Step3 should show an error 1`] = ` + + +

+ Test error +

+
+ + + Save + +
+`; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx new file mode 100644 index 00000000000000..6b7e2391e0301c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.test.tsx @@ -0,0 +1,147 @@ +/* + * 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 { mockUseEffects } from '../../../jest.helpers'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { kfetch } from 'ui/kfetch'; +import { AlertsConfiguration, AlertsConfigurationProps } from './configuration'; + +jest.mock('ui/kfetch', () => ({ + kfetch: jest.fn(), +})); + +const defaultProps: AlertsConfigurationProps = { + emailAddress: 'test@elastic.co', + onDone: jest.fn(), +}; + +describe('Configuration', () => { + it('should render high level steps', () => { + const component = shallow(); + expect(component.find('EuiSteps').shallow()).toMatchSnapshot(); + }); + + function getStep(component: ShallowWrapper, index: number) { + return component + .find('EuiSteps') + .shallow() + .find('EuiStep') + .at(index) + .children() + .shallow(); + } + + describe('shallow view', () => { + it('should render step 1', () => { + const component = shallow(); + const stepOne = getStep(component, 0); + expect(stepOne).toMatchSnapshot(); + }); + + it('should render step 2', () => { + const component = shallow(); + const stepTwo = getStep(component, 1); + expect(stepTwo).toMatchSnapshot(); + }); + + it('should render step 3', () => { + const component = shallow(); + const stepThree = getStep(component, 2); + expect(stepThree).toMatchSnapshot(); + }); + }); + + describe('selected action', () => { + const actionId = 'a123b'; + let component: ShallowWrapper; + beforeEach(async () => { + mockUseEffects(2); + + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [ + { + actionTypeId: '.email', + id: actionId, + config: {}, + }, + ], + }; + }); + + component = shallow(); + }); + + it('reflect in Step1', async () => { + const steps = component.find('EuiSteps').dive(); + expect( + steps + .find('EuiStep') + .at(0) + .prop('title') + ).toBe('Select email action'); + expect(steps.find('Step1').prop('selectedEmailActionId')).toBe(actionId); + }); + + it('should enable Step2', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step2').prop('isDisabled')).toBe(false); + }); + + it('should enable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(false); + }); + }); + + describe('edit action', () => { + let component: ShallowWrapper; + beforeEach(async () => { + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [], + }; + }); + + component = shallow(); + }); + + it('disable Step2', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step2').prop('isDisabled')).toBe(true); + }); + + it('disable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(true); + }); + }); + + describe('no email address', () => { + let component: ShallowWrapper; + beforeEach(async () => { + (kfetch as jest.Mock).mockImplementation(() => { + return { + data: [ + { + actionTypeId: '.email', + id: 'actionId', + config: {}, + }, + ], + }; + }); + + component = shallow(); + }); + + it('should disable Step3', async () => { + const steps = component.find('EuiSteps').dive(); + expect(steps.find('Step3').prop('isDisabled')).toBe(true); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx new file mode 100644 index 00000000000000..0933cd22db7c9f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/configuration.tsx @@ -0,0 +1,193 @@ +/* + * 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, { ReactNode } from 'react'; +import { kfetch } from 'ui/kfetch'; +import { EuiSteps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; +import { getMissingFieldErrors } from '../../../lib/form_validation'; +import { Step1 } from './step1'; +import { Step2 } from './step2'; +import { Step3 } from './step3'; + +export interface AlertsConfigurationProps { + emailAddress: string; + onDone: Function; +} + +export interface StepResult { + title: string; + children: ReactNode; + status: any; +} + +export interface AlertsConfigurationForm { + email: string | null; +} + +export const NEW_ACTION_ID = '__new__'; + +export const AlertsConfiguration: React.FC = ( + props: AlertsConfigurationProps +) => { + const { onDone } = props; + + const [emailActions, setEmailActions] = React.useState([]); + const [selectedEmailActionId, setSelectedEmailActionId] = React.useState(''); + const [editAction, setEditAction] = React.useState(null); + const [emailAddress, setEmailAddress] = React.useState(props.emailAddress); + const [formErrors, setFormErrors] = React.useState({ email: null }); + const [showFormErrors, setShowFormErrors] = React.useState(false); + const [isSaving, setIsSaving] = React.useState(false); + const [saveError, setSaveError] = React.useState(''); + + React.useEffect(() => { + async function fetchData() { + await fetchEmailActions(); + } + + fetchData(); + }, []); + + React.useEffect(() => { + setFormErrors(getMissingFieldErrors({ email: emailAddress }, { email: '' })); + }, [emailAddress]); + + async function fetchEmailActions() { + const kibanaActions = await kfetch({ + method: 'GET', + pathname: `/api/action/_find`, + }); + + const actions = kibanaActions.data.filter( + (action: ActionResult) => action.actionTypeId === ALERT_ACTION_TYPE_EMAIL + ); + if (actions.length > 0) { + setSelectedEmailActionId(actions[0].id); + } else { + setSelectedEmailActionId(NEW_ACTION_ID); + } + setEmailActions(actions); + } + + async function save() { + if (emailAddress.length === 0) { + setShowFormErrors(true); + return; + } + setIsSaving(true); + setShowFormErrors(false); + + try { + await kfetch({ + method: 'POST', + pathname: `/api/monitoring/v1/alerts`, + body: JSON.stringify({ selectedEmailActionId, emailAddress }), + }); + } catch (err) { + setIsSaving(false); + setSaveError( + err?.body?.message || + i18n.translate('xpack.monitoring.alerts.configuration.unknownError', { + defaultMessage: 'Something went wrong. Please consult the server logs.', + }) + ); + return; + } + + onDone(); + } + + function isStep2Disabled() { + return isStep2AndStep3Disabled(); + } + + function isStep3Disabled() { + return isStep2AndStep3Disabled() || !emailAddress || emailAddress.length === 0; + } + + function isStep2AndStep3Disabled() { + return !!editAction || !selectedEmailActionId || selectedEmailActionId === NEW_ACTION_ID; + } + + function getStep2Status() { + const isDisabled = isStep2AndStep3Disabled(); + + if (isDisabled) { + return 'disabled' as const; + } + + if (emailAddress && emailAddress.length) { + return 'complete' as const; + } + + return 'incomplete' as const; + } + + function getStep1Status() { + if (editAction) { + return 'incomplete' as const; + } + + return selectedEmailActionId ? ('complete' as const) : ('incomplete' as const); + } + + const steps = [ + { + title: emailActions.length + ? i18n.translate('xpack.monitoring.alerts.configuration.selectEmailAction', { + defaultMessage: 'Select email action', + }) + : i18n.translate('xpack.monitoring.alerts.configuration.createEmailAction', { + defaultMessage: 'Create email action', + }), + children: ( + await fetchEmailActions()} + emailActions={emailActions} + selectedEmailActionId={selectedEmailActionId} + setSelectedEmailActionId={setSelectedEmailActionId} + emailAddress={emailAddress} + editAction={editAction} + setEditAction={setEditAction} + /> + ), + status: getStep1Status(), + }, + { + title: i18n.translate('xpack.monitoring.alerts.configuration.setEmailAddress', { + defaultMessage: 'Set the email to receive alerts', + }), + status: getStep2Status(), + children: ( + + ), + }, + { + title: i18n.translate('xpack.monitoring.alerts.configuration.confirm', { + defaultMessage: 'Confirm and save', + }), + status: getStep2Status(), + children: ( + + ), + }, + ]; + + return ( +
+ +
+ ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts new file mode 100644 index 00000000000000..7a96c6e324ab36 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { AlertsConfiguration } from './configuration'; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx new file mode 100644 index 00000000000000..650294c29e9a50 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.test.tsx @@ -0,0 +1,338 @@ +/* + * 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 { omit, pick } from 'lodash'; +import '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { GetStep1Props } from './step1'; +import { EmailActionData } from '../manage_email_action'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; + +let Step1: React.FC; +let NEW_ACTION_ID: string; + +function setModules() { + Step1 = require('./step1').Step1; + NEW_ACTION_ID = require('./configuration').NEW_ACTION_ID; +} + +describe('Step1', () => { + const emailActions = [ + { + id: '1', + actionTypeId: '1abc', + name: 'Testing', + config: {}, + }, + ]; + const selectedEmailActionId = emailActions[0].id; + const setSelectedEmailActionId = jest.fn(); + const emailAddress = 'test@test.com'; + const editAction = null; + const setEditAction = jest.fn(); + const onActionDone = jest.fn(); + + const defaultProps: GetStep1Props = { + onActionDone, + emailActions, + selectedEmailActionId, + setSelectedEmailActionId, + emailAddress, + editAction, + setEditAction, + }; + + beforeEach(() => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: () => { + return {}; + }, + })); + setModules(); + }); + }); + + it('should render normally', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + describe('creating', () => { + it('should render a create form', () => { + const customProps = { + emailActions: [], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + it('should render the select box if at least one action exists', () => { + const customProps = { + emailActions: [ + { + id: 'foo', + actionTypeId: '.email', + name: '', + config: {}, + }, + ], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(); + expect(component.find('EuiSuperSelect').exists()).toBe(true); + }); + + it('should send up the create to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + emailActions: [], + selectedEmailActionId: NEW_ACTION_ID, + }; + + const component = shallow(); + + const data: EmailActionData = { + service: 'gmail', + host: 'smtp.gmail.com', + port: 465, + secure: true, + from: 'test@test.com', + user: 'user@user.com', + password: 'password', + }; + + const createEmailAction: (data: EmailActionData) => void = component + .find('ManageEmailAction') + .prop('createEmailAction'); + createEmailAction(data); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'POST', + pathname: `/api/action`, + body: JSON.stringify({ + name: 'Email action for Stack Monitoring alerts', + actionTypeId: ALERT_ACTION_TYPE_EMAIL, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + }); + }); + + describe('editing', () => { + it('should allow for editing', () => { + const customProps = { + editAction: emailActions[0], + }; + + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + + it('should send up the edit to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + editAction: emailActions[0], + }; + + const component = shallow(); + + const data: EmailActionData = { + service: 'gmail', + host: 'smtp.gmail.com', + port: 465, + secure: true, + from: 'test@test.com', + user: 'user@user.com', + password: 'password', + }; + + const createEmailAction: (data: EmailActionData) => void = component + .find('ManageEmailAction') + .prop('createEmailAction'); + createEmailAction(data); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'PUT', + pathname: `/api/action/${emailActions[0].id}`, + body: JSON.stringify({ + name: emailActions[0].name, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + }); + }); + + describe('testing', () => { + it('should allow for testing', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: jest.fn().mockImplementation(arg => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }), + })); + setModules(); + }); + + const component = shallow(); + + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(false); + component + .find('EuiButton') + .at(1) + .simulate('click'); + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(true); + await component.update(); + expect( + component + .find('EuiButton') + .at(1) + .prop('isLoading') + ).toBe(false); + }); + + it('should show a successful test', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { status: 'ok' }; + } + return {}; + }, + })); + setModules(); + }); + + const component = shallow(); + + component + .find('EuiButton') + .at(1) + .simulate('click'); + await component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should show a failed test error', async () => { + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch: (arg: any) => { + if (arg.pathname === '/api/action/1/_execute') { + return { message: 'Very detailed error message' }; + } + return {}; + }, + })); + setModules(); + }); + + const component = shallow(); + + component + .find('EuiButton') + .at(1) + .simulate('click'); + await component.update(); + expect(component).toMatchSnapshot(); + }); + + it('should not allow testing if there is no email address', () => { + const customProps = { + emailAddress: '', + }; + const component = shallow(); + expect( + component + .find('EuiButton') + .at(1) + .prop('isDisabled') + ).toBe(true); + }); + + it('should should a tooltip if there is no email address', () => { + const customProps = { + emailAddress: '', + }; + const component = shallow(); + expect(component.find('EuiToolTip')).toMatchSnapshot(); + }); + }); + + describe('deleting', () => { + it('should send up the delete to the server', async () => { + const kfetch = jest.fn().mockImplementation(() => {}); + jest.isolateModules(() => { + jest.doMock('ui/kfetch', () => ({ + kfetch, + })); + setModules(); + }); + + const customProps = { + setSelectedEmailActionId: jest.fn(), + onActionDone: jest.fn(), + }; + const component = shallow(); + + await component + .find('EuiButton') + .at(2) + .simulate('click'); + await component.update(); + + expect(kfetch).toHaveBeenCalledWith({ + method: 'DELETE', + pathname: `/api/action/${emailActions[0].id}`, + }); + + expect(customProps.setSelectedEmailActionId).toHaveBeenCalledWith(''); + expect(customProps.onActionDone).toHaveBeenCalled(); + expect( + component + .find('EuiButton') + .at(2) + .prop('isLoading') + ).toBe(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx new file mode 100644 index 00000000000000..fc051a68e29f35 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step1.tsx @@ -0,0 +1,334 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiText, + EuiSpacer, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSuperSelect, + EuiToolTip, + EuiCallOut, +} from '@elastic/eui'; +import { kfetch } from 'ui/kfetch'; +import { omit, pick } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../../plugins/actions/common'; +import { ManageEmailAction, EmailActionData } from '../manage_email_action'; +import { ALERT_ACTION_TYPE_EMAIL } from '../../../../common/constants'; +import { NEW_ACTION_ID } from './configuration'; + +export interface GetStep1Props { + onActionDone: () => Promise; + emailActions: ActionResult[]; + selectedEmailActionId: string; + setSelectedEmailActionId: (id: string) => void; + emailAddress: string; + editAction: ActionResult | null; + setEditAction: (action: ActionResult | null) => void; +} + +export const Step1: React.FC = (props: GetStep1Props) => { + const [isTesting, setIsTesting] = React.useState(false); + const [isDeleting, setIsDeleting] = React.useState(false); + const [testingStatus, setTestingStatus] = React.useState(null); + const [fullTestingError, setFullTestingError] = React.useState(''); + + async function createEmailAction(data: EmailActionData) { + if (props.editAction) { + await kfetch({ + method: 'PUT', + pathname: `/api/action/${props.editAction.id}`, + body: JSON.stringify({ + name: props.editAction.name, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + props.setEditAction(null); + } else { + await kfetch({ + method: 'POST', + pathname: '/api/action', + body: JSON.stringify({ + name: i18n.translate('xpack.monitoring.alerts.configuration.emailAction.name', { + defaultMessage: 'Email action for Stack Monitoring alerts', + }), + actionTypeId: ALERT_ACTION_TYPE_EMAIL, + config: omit(data, ['user', 'password']), + secrets: pick(data, ['user', 'password']), + }), + }); + } + + await props.onActionDone(); + } + + async function deleteEmailAction(id: string) { + setIsDeleting(true); + + await kfetch({ + method: 'DELETE', + pathname: `/api/action/${id}`, + }); + + if (props.editAction && props.editAction.id === id) { + props.setEditAction(null); + } + if (props.selectedEmailActionId === id) { + props.setSelectedEmailActionId(''); + } + await props.onActionDone(); + setIsDeleting(false); + setTestingStatus(null); + } + + async function testEmailAction() { + setIsTesting(true); + setTestingStatus(null); + + const params = { + subject: 'Kibana alerting test configuration', + message: `This is a test for the configured email action for Kibana alerting.`, + to: [props.emailAddress], + }; + + const result = await kfetch({ + method: 'POST', + pathname: `/api/action/${props.selectedEmailActionId}/_execute`, + body: JSON.stringify({ params }), + }); + if (result.status === 'ok') { + setTestingStatus(true); + } else { + setTestingStatus(false); + setFullTestingError(result.message); + } + setIsTesting(false); + } + + function getTestButton() { + const isTestingDisabled = !props.emailAddress || props.emailAddress.length === 0; + const testBtn = ( + + {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.buttonText', { + defaultMessage: 'Test', + })} + + ); + + if (isTestingDisabled) { + return ( + + {testBtn} + + ); + } + + return testBtn; + } + + if (props.editAction) { + return ( + + +

+ {i18n.translate('xpack.monitoring.alerts.configuration.step1.editAction', { + defaultMessage: 'Edit the action below.', + })} +

+
+ + await createEmailAction(data)} + cancel={() => props.setEditAction(null)} + isNew={false} + action={props.editAction} + /> +
+ ); + } + + const newAction = ( + + {i18n.translate('xpack.monitoring.alerts.configuration.newActionDropdownDisplay', { + defaultMessage: 'Create new email action...', + })} + + ); + + const options = [ + ...props.emailActions.map(action => { + const actionLabel = i18n.translate( + 'xpack.monitoring.alerts.configuration.selectAction.inputDisplay', + { + defaultMessage: 'From: {from}, Service: {service}', + values: { + service: action.config.service, + from: action.config.from, + }, + } + ); + + return { + value: action.id, + inputDisplay: {actionLabel}, + dropdownDisplay: {actionLabel}, + }; + }), + { + value: NEW_ACTION_ID, + inputDisplay: newAction, + dropdownDisplay: newAction, + }, + ]; + + let selectBox: React.ReactNode | null = ( + props.setSelectedEmailActionId(id)} + hasDividers + /> + ); + let createNew = null; + if (props.selectedEmailActionId === NEW_ACTION_ID) { + createNew = ( + + await createEmailAction(data)} + isNew={true} + /> + + ); + + // If there are no actions, do not show the select box as there are no choices + if (props.emailActions.length === 0) { + selectBox = null; + } else { + // Otherwise, add a spacer + selectBox = ( + + {selectBox} + + + ); + } + } + + let manageConfiguration = null; + const selectedEmailAction = props.emailActions.find( + action => action.id === props.selectedEmailActionId + ); + + if ( + props.selectedEmailActionId !== NEW_ACTION_ID && + props.selectedEmailActionId && + selectedEmailAction + ) { + let testingStatusUi = null; + if (testingStatus === true) { + testingStatusUi = ( + + + +

+ {i18n.translate('xpack.monitoring.alerts.configuration.testConfiguration.success', { + defaultMessage: 'Looks good on our end!', + })} +

+
+
+ ); + } else if (testingStatus === false) { + testingStatusUi = ( + + + +

{fullTestingError}

+
+
+ ); + } + + manageConfiguration = ( + + + + + { + const editAction = + props.emailActions.find(action => action.id === props.selectedEmailActionId) || + null; + props.setEditAction(editAction); + }} + > + {i18n.translate( + 'xpack.monitoring.alerts.configuration.editConfiguration.buttonText', + { + defaultMessage: 'Edit', + } + )} + + + {getTestButton()} + + deleteEmailAction(props.selectedEmailActionId)} + isLoading={isDeleting} + > + {i18n.translate( + 'xpack.monitoring.alerts.configuration.deleteConfiguration.buttonText', + { + defaultMessage: 'Delete', + } + )} + + + + {testingStatusUi} + + ); + } + + return ( + + {selectBox} + {manageConfiguration} + {createNew} + + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx new file mode 100644 index 00000000000000..14e3cb078f9cca --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.test.tsx @@ -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 React from 'react'; +import '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { Step2, GetStep2Props } from './step2'; + +describe('Step2', () => { + const defaultProps: GetStep2Props = { + emailAddress: 'test@test.com', + setEmailAddress: jest.fn(), + showFormErrors: false, + formErrors: { email: null }, + isDisabled: false, + }; + + it('should render normally', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should set the email address properly', () => { + const newEmail = 'email@email.com'; + const component = shallow(); + component.find('EuiFieldText').simulate('change', { target: { value: newEmail } }); + expect(defaultProps.setEmailAddress).toHaveBeenCalledWith(newEmail); + }); + + it('should show form errors', () => { + const customProps = { + showFormErrors: true, + formErrors: { + email: 'This is required', + }, + }; + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should disable properly', () => { + const customProps = { + isDisabled: true, + }; + const component = shallow(); + expect(component.find('EuiFieldText').prop('disabled')).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx new file mode 100644 index 00000000000000..974dd8513d231d --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step2.tsx @@ -0,0 +1,38 @@ +/* + * 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 { EuiForm, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AlertsConfigurationForm } from './configuration'; + +export interface GetStep2Props { + emailAddress: string; + setEmailAddress: (email: string) => void; + showFormErrors: boolean; + formErrors: AlertsConfigurationForm; + isDisabled: boolean; +} + +export const Step2: React.FC = (props: GetStep2Props) => { + return ( + + + props.setEmailAddress(e.target.value)} + /> + + + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx new file mode 100644 index 00000000000000..9b1304c42a507d --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 '../../../jest.helpers'; +import { shallow } from 'enzyme'; +import { Step3 } from './step3'; + +describe('Step3', () => { + const defaultProps = { + isSaving: false, + isDisabled: false, + save: jest.fn(), + error: null, + }; + + it('should render normally', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should save properly', () => { + const component = shallow(); + component.find('EuiButton').simulate('click'); + expect(defaultProps.save).toHaveBeenCalledWith(); + }); + + it('should show a saving state', () => { + const customProps = { isSaving: true }; + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should show a disabled state', () => { + const customProps = { isDisabled: true }; + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should show an error', () => { + const customProps = { error: 'Test error' }; + const component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx new file mode 100644 index 00000000000000..80acb8992cbc16 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/configuration/step3.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment } from 'react'; +import { EuiButton, EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface GetStep3Props { + isSaving: boolean; + isDisabled: boolean; + save: () => void; + error: string | null; +} + +export const Step3: React.FC = (props: GetStep3Props) => { + let errorUi = null; + if (props.error) { + errorUi = ( + + +

{props.error}

+
+ +
+ ); + } + + return ( + + {errorUi} + + {i18n.translate('xpack.monitoring.alerts.configuration.save', { + defaultMessage: 'Save', + })} + + + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx new file mode 100644 index 00000000000000..2bd9804795cb56 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/manage_email_action.tsx @@ -0,0 +1,301 @@ +/* + * 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, { Fragment } from 'react'; +import { + EuiForm, + EuiFormRow, + EuiFieldText, + EuiLink, + EuiSpacer, + EuiFieldNumber, + EuiFieldPassword, + EuiSwitch, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionResult } from '../../../../../../plugins/actions/common'; +import { getMissingFieldErrors, hasErrors, getRequiredFieldError } from '../../lib/form_validation'; +import { ALERT_EMAIL_SERVICES } from '../../../common/constants'; + +export interface EmailActionData { + service: string; + host: string; + port?: number; + secure: boolean; + from: string; + user: string; + password: string; +} + +interface ManageActionModalProps { + createEmailAction: (handler: EmailActionData) => void; + cancel?: () => void; + isNew: boolean; + action?: ActionResult | null; +} + +const DEFAULT_DATA: EmailActionData = { + service: '', + host: '', + port: 0, + secure: false, + from: '', + user: '', + password: '', +}; + +const CREATE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.createLabel', { + defaultMessage: 'Create email action', +}); +const SAVE_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.saveLabel', { + defaultMessage: 'Save email action', +}); +const CANCEL_LABEL = i18n.translate('xpack.monitoring.alerts.migrate.manageAction.cancelLabel', { + defaultMessage: 'Cancel', +}); + +const NEW_SERVICE_ID = '__new__'; + +export const ManageEmailAction: React.FC = ( + props: ManageActionModalProps +) => { + const { createEmailAction, cancel, isNew, action } = props; + + const defaultData = Object.assign({}, DEFAULT_DATA, action ? action.config : {}); + const [isSaving, setIsSaving] = React.useState(false); + const [showErrors, setShowErrors] = React.useState(false); + const [errors, setErrors] = React.useState( + getMissingFieldErrors(defaultData, DEFAULT_DATA) + ); + const [data, setData] = React.useState(defaultData); + const [createNewService, setCreateNewService] = React.useState(false); + const [newService, setNewService] = React.useState(''); + + React.useEffect(() => { + const missingFieldErrors = getMissingFieldErrors(data, DEFAULT_DATA); + if (!missingFieldErrors.service) { + if (data.service === NEW_SERVICE_ID && !newService) { + missingFieldErrors.service = getRequiredFieldError('service'); + } + } + setErrors(missingFieldErrors); + }, [data, newService]); + + async function saveEmailAction() { + setShowErrors(true); + if (!hasErrors(errors)) { + setShowErrors(false); + setIsSaving(true); + const mergedData = { + ...data, + service: data.service === NEW_SERVICE_ID ? newService : data.service, + }; + try { + await createEmailAction(mergedData); + } catch (err) { + setErrors({ + general: err.body.message, + }); + } + } + } + + const serviceOptions = ALERT_EMAIL_SERVICES.map(service => ({ + value: service, + inputDisplay: {service}, + dropdownDisplay: {service}, + })); + + serviceOptions.push({ + value: NEW_SERVICE_ID, + inputDisplay: ( + + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addingNewServiceText', { + defaultMessage: 'Adding new service...', + })} + + ), + dropdownDisplay: ( + + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.addNewServiceText', { + defaultMessage: 'Add new service...', + })} + + ), + }); + + let addNewServiceUi = null; + if (createNewService) { + addNewServiceUi = ( + + + setNewService(e.target.value)} + isInvalid={showErrors} + /> + + ); + } + + return ( + + + {i18n.translate('xpack.monitoring.alerts.migrate.manageAction.serviceHelpText', { + defaultMessage: 'Find out more', + })} + + } + error={errors.service} + isInvalid={showErrors && !!errors.service} + > + + { + if (id === NEW_SERVICE_ID) { + setCreateNewService(true); + setData({ ...data, service: NEW_SERVICE_ID }); + } else { + setCreateNewService(false); + setData({ ...data, service: id }); + } + }} + hasDividers + isInvalid={showErrors && !!errors.service} + /> + {addNewServiceUi} + + + + + setData({ ...data, host: e.target.value })} + isInvalid={showErrors && !!errors.host} + /> + + + + setData({ ...data, port: parseInt(e.target.value, 10) })} + isInvalid={showErrors && !!errors.port} + /> + + + + setData({ ...data, secure: e.target.checked })} + /> + + + + setData({ ...data, from: e.target.value })} + isInvalid={showErrors && !!errors.from} + /> + + + + setData({ ...data, user: e.target.value })} + isInvalid={showErrors && !!errors.user} + /> + + + + setData({ ...data, password: e.target.value })} + isInvalid={showErrors && !!errors.password} + /> + + + + + + + + {isNew ? CREATE_LABEL : SAVE_LABEL} + + + {!action || isNew ? null : ( + + {CANCEL_LABEL} + + )} + + + ); +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx new file mode 100644 index 00000000000000..258a5b68db372e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 { shallow } from 'enzyme'; +import { kfetch } from 'ui/kfetch'; +import { AlertsStatus, AlertsStatusProps } from './status'; +import { ALERT_TYPE_PREFIX } from '../../../common/constants'; +import { getSetupModeState } from '../../lib/setup_mode'; +import { mockUseEffects } from '../../jest.helpers'; + +jest.mock('../../lib/setup_mode', () => ({ + getSetupModeState: jest.fn(), + addSetupModeCallback: jest.fn(), + toggleSetupMode: jest.fn(), +})); + +jest.mock('ui/kfetch', () => ({ + kfetch: jest.fn(), +})); + +const defaultProps: AlertsStatusProps = { + clusterUuid: '1adsb23', + emailAddress: 'test@elastic.co', +}; + +describe('Status', () => { + beforeEach(() => { + mockUseEffects(2); + + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: false, + }); + + (kfetch as jest.Mock).mockImplementation(({ pathname }) => { + if (pathname === '/internal/security/api_key/privileges') { + return { areApiKeysEnabled: true }; + } + return { + data: [], + }; + }); + }); + + it('should render without setup mode', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + + it('should render a flyout when clicking the link', async () => { + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: true, + }); + + const component = shallow(); + component.find('EuiLink').simulate('click'); + await component.update(); + expect(component.find('EuiFlyout')).toMatchSnapshot(); + }); + + it('should render a success message if all alerts have been migrated and in setup mode', async () => { + (kfetch as jest.Mock).mockReturnValue({ + data: [ + { + alertTypeId: ALERT_TYPE_PREFIX, + }, + ], + }); + + (getSetupModeState as jest.Mock).mockReturnValue({ + enabled: true, + }); + + const component = shallow(); + await component.update(); + expect(component.find('EuiCallOut')).toMatchSnapshot(); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx new file mode 100644 index 00000000000000..0ee0015ed39a70 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/components/alerts/status.tsx @@ -0,0 +1,203 @@ +/* + * 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, { Fragment } from 'react'; +import { kfetch } from 'ui/kfetch'; +import { + EuiSpacer, + EuiCallOut, + EuiTitle, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { Alert } from '../../../../alerting/server/types'; +import { getSetupModeState, addSetupModeCallback, toggleSetupMode } from '../../lib/setup_mode'; +import { NUMBER_OF_MIGRATED_ALERTS, ALERT_TYPE_PREFIX } from '../../../common/constants'; +import { AlertsConfiguration } from './configuration'; + +export interface AlertsStatusProps { + clusterUuid: string; + emailAddress: string; +} + +export const AlertsStatus: React.FC = (props: AlertsStatusProps) => { + const { emailAddress } = props; + + const [setupModeEnabled, setSetupModeEnabled] = React.useState(getSetupModeState().enabled); + const [kibanaAlerts, setKibanaAlerts] = React.useState([]); + const [showMigrationFlyout, setShowMigrationFlyout] = React.useState(false); + const [isSecurityConfigured, setIsSecurityConfigured] = React.useState(false); + + React.useEffect(() => { + async function fetchAlertsStatus() { + const alerts = await kfetch({ method: 'GET', pathname: `/api/alert/_find` }); + const monitoringAlerts = alerts.data.filter((alert: Alert) => + alert.alertTypeId.startsWith(ALERT_TYPE_PREFIX) + ); + setKibanaAlerts(monitoringAlerts); + } + + fetchAlertsStatus(); + fetchSecurityConfigured(); + }, [setupModeEnabled, showMigrationFlyout]); + + React.useEffect(() => { + if (!setupModeEnabled && showMigrationFlyout) { + setShowMigrationFlyout(false); + } + }, [setupModeEnabled, showMigrationFlyout]); + + async function fetchSecurityConfigured() { + const response = await kfetch({ pathname: '/internal/security/api_key/privileges' }); + setIsSecurityConfigured(response.areApiKeysEnabled); + } + + addSetupModeCallback(() => setSetupModeEnabled(getSetupModeState().enabled)); + + function enterSetupModeAndOpenFlyout() { + toggleSetupMode(true); + setShowMigrationFlyout(true); + } + + function getSecurityConfigurationErrorUi() { + if (isSecurityConfigured) { + return null; + } + + const link = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/security-settings.html#api-key-service-settings`; + return ( + + + +

+ + {i18n.translate( + 'xpack.monitoring.alerts.configuration.securityConfigurationError.docsLinkLabel', + { + defaultMessage: 'docs', + } + )} + + ), + }} + /> +

+
+
+ ); + } + + function renderContent() { + let flyout = null; + if (showMigrationFlyout) { + flyout = ( + setShowMigrationFlyout(false)} aria-labelledby="flyoutTitle"> + + +

+ {i18n.translate('xpack.monitoring.alerts.status.flyoutTitle', { + defaultMessage: 'Monitoring alerts', + })} +

+
+ +

+ {i18n.translate('xpack.monitoring.alerts.status.flyoutSubtitle', { + defaultMessage: 'Configure an email server and email address to receive alerts.', + })} +

+
+ {getSecurityConfigurationErrorUi()} +
+ + setShowMigrationFlyout(false)} + /> + +
+ ); + } + + const allMigrated = kibanaAlerts.length === NUMBER_OF_MIGRATED_ALERTS; + if (allMigrated) { + if (setupModeEnabled) { + return ( + + +

+ + {i18n.translate('xpack.monitoring.alerts.status.manage', { + defaultMessage: 'Want to make changes? Click here.', + })} + +

+
+ {flyout} +
+ ); + } + } else { + return ( + + +

+ + {i18n.translate('xpack.monitoring.alerts.status.needToMigrate', { + defaultMessage: 'Migrate cluster alerts to our new alerting platform.', + })} + +

+
+ {flyout} +
+ ); + } + } + + const content = renderContent(); + if (content) { + return ( + + {content} + + + ); + } + + return null; +}; diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js index 33b26c7ec56e00..a8001638f4399d 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/alerts_panel.js @@ -4,11 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { Fragment } from 'react'; +import moment from 'moment-timezone'; import { FormattedAlert } from 'plugins/monitoring/components/alerts/formatted_alert'; import { mapSeverity } from 'plugins/monitoring/components/alerts/map_severity'; import { formatTimestampToDuration } from '../../../../common/format_timestamp_to_duration'; -import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; +import { + CALCULATE_DURATION_SINCE, + KIBANA_ALERTING_ENABLED, + ALERT_TYPE_LICENSE_EXPIRATION, + CALCULATE_DURATION_UNTIL, +} from '../../../../common/constants'; import { formatDateTimeLocal } from '../../../../common/formatting'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -21,6 +27,7 @@ import { EuiText, EuiSpacer, EuiCallOut, + EuiLink, } from '@elastic/eui'; export function AlertsPanel({ alerts, changeUrl }) { @@ -82,9 +89,52 @@ export function AlertsPanel({ alerts, changeUrl }) { ); } - const topAlertItems = alerts.map((item, index) => ( - - )); + const alertsList = KIBANA_ALERTING_ENABLED + ? alerts.map((alert, idx) => { + const callOutProps = mapSeverity(alert.severity); + let message = alert.message + // scan message prefix and replace relative times + // \w: Matches any alphanumeric character from the basic Latin alphabet, including the underscore. Equivalent to [A-Za-z0-9_]. + .replace( + '#relative', + formatTimestampToDuration(alert.expirationTime, CALCULATE_DURATION_UNTIL) + ) + .replace('#absolute', moment.tz(alert.expirationTime, moment.tz.guess()).format('LLL z')); + + if (!alert.isFiring) { + callOutProps.title = i18n.translate( + 'xpack.monitoring.cluster.overview.alertsPanel.severityIconTitle', + { + defaultMessage: '{severityIconTitle} (resolved {time} ago)', + values: { + severityIconTitle: callOutProps.title, + time: formatTimestampToDuration(alert.resolvedMS, CALCULATE_DURATION_SINCE), + }, + } + ); + callOutProps.color = 'success'; + callOutProps.iconType = 'check'; + } else { + if (alert.type === ALERT_TYPE_LICENSE_EXPIRATION) { + message = ( + + {message} +   + Please update your license + + ); + } + } + + return ( + +

{message}

+
+ ); + }) + : alerts.map((item, index) => ( + + )); return (
@@ -109,7 +159,7 @@ export function AlertsPanel({ alerts, changeUrl }) { - {topAlertItems} + {alertsList}
); diff --git a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js index cad4bbf411c347..eee51c416d11eb 100644 --- a/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js +++ b/x-pack/legacy/plugins/monitoring/public/components/cluster/overview/index.js @@ -10,15 +10,22 @@ import { KibanaPanel } from './kibana_panel'; import { LogstashPanel } from './logstash_panel'; import { AlertsPanel } from './alerts_panel'; import { BeatsPanel } from './beats_panel'; - import { EuiPage, EuiPageBody, EuiScreenReaderOnly } from '@elastic/eui'; import { ApmPanel } from './apm_panel'; -import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertsStatus } from '../../alerts/status'; +import { + STANDALONE_CLUSTER_CLUSTER_UUID, + KIBANA_ALERTING_ENABLED, +} from '../../../../common/constants'; export function Overview(props) { const isFromStandaloneCluster = props.cluster.cluster_uuid === STANDALONE_CLUSTER_CLUSTER_UUID; + const kibanaAlerts = KIBANA_ALERTING_ENABLED ? ( + + ) : null; + return ( @@ -30,6 +37,9 @@ export function Overview(props) { /> + + {kibanaAlerts} + {!isFromStandaloneCluster ? ( diff --git a/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts b/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts new file mode 100644 index 00000000000000..46ba603d301382 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/jest.helpers.ts @@ -0,0 +1,36 @@ +/* + * 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'; + +/** + * Suppress React 16.8 act() warnings globally. + * The react teams fix won't be out of alpha until 16.9.0. + * https://github.com/facebook/react/issues/14769#issuecomment-514589856 + */ +const consoleError = console.error; // eslint-disable-line no-console +beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation((...args) => { + if (!args[0].includes('Warning: An update to %s inside a test was not wrapped in act')) { + consoleError(...args); + } + }); +}); + +export function mockUseEffects(count = 1) { + const spy = jest.spyOn(React, 'useEffect'); + for (let i = 0; i < count; i++) { + spy.mockImplementationOnce(f => f()); + } +} + +// export function mockUseEffectForDeps(deps, count = 1) { +// const spy = jest.spyOn(React, 'useEffect'); +// for (let i = 0; i < count; i++) { +// spy.mockImplementationOnce((f, depList) => { + +// }); +// } +// } diff --git a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js b/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx similarity index 94% rename from x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js rename to x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx index 9a51a885969264..22ce32103c208f 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/ajax_error_handler.tsx @@ -7,12 +7,13 @@ import React from 'react'; import { contains } from 'lodash'; import { toastNotifications } from 'ui/notify'; +// @ts-ignore import { formatMsg } from 'ui/notify/lib'; import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -export function formatMonitoringError(err) { +export function formatMonitoringError(err: any) { // TODO: We should stop using Boom for errors and instead write a custom handler to return richer error objects // then we can do better messages, such as highlighting the Cluster UUID instead of requiring it be part of the message if (err.status && err.status !== -1 && err.data) { @@ -33,10 +34,10 @@ export function formatMonitoringError(err) { return formatMsg(err); } -export function ajaxErrorHandlersProvider($injector) { +export function ajaxErrorHandlersProvider($injector: any) { const kbnUrl = $injector.get('kbnUrl'); - return err => { + return (err: any) => { if (err.status === 403) { // redirect to error message view kbnUrl.redirect('access-denied'); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts b/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts new file mode 100644 index 00000000000000..98d56f9790be47 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/public/lib/form_validation.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { isString, isNumber, capitalize } from 'lodash'; + +export function getRequiredFieldError(field: string): string { + return i18n.translate('xpack.monitoring.alerts.migrate.manageAction.requiredFieldError', { + defaultMessage: '{field} is a required field.', + values: { + field: capitalize(field), + }, + }); +} + +export function getMissingFieldErrors(data: any, defaultData: any) { + const errors: any = {}; + + for (const key in data) { + if (!data.hasOwnProperty(key)) { + continue; + } + + if (isString(defaultData[key])) { + if (!data[key] || data[key].length === 0) { + errors[key] = getRequiredFieldError(key); + } + } else if (isNumber(defaultData[key])) { + if (isNaN(data[key]) || data[key] === 0) { + errors[key] = getRequiredFieldError(key); + } + } + } + + return errors; +} + +export function hasErrors(errors: any) { + for (const error in errors) { + if (error.length) { + return true; + } + } + return false; +} diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js index aa931368b34c28..4a2b470f04c729 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js @@ -90,7 +90,7 @@ describe('setup_mode', () => { } catch (err) { error = err; } - expect(error).toEqual( + expect(error.message).toEqual( 'Unable to interact with setup ' + 'mode because the angular injector was not previously set. This needs to be ' + 'set by calling `initSetupModeState`.' @@ -255,9 +255,9 @@ describe('setup_mode', () => { await toggleSetupMode(true); injectorModulesMock.$http.post.mockClear(); await updateSetupModeData(undefined, true); - expect( - injectorModulesMock.$http.post - ).toHaveBeenCalledWith('../api/monitoring/v1/setup/collection/cluster', { ccs: undefined }); + const url = '../api/monitoring/v1/setup/collection/cluster'; + const args = { ccs: undefined }; + expect(injectorModulesMock.$http.post).toHaveBeenCalledWith(url, args); }); }); }); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx similarity index 76% rename from x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js rename to x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx index 41aae013076178..d805c10247b2ed 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.tsx @@ -6,31 +6,49 @@ import React from 'react'; import { render } from 'react-dom'; -import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { get, contains } from 'lodash'; import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; import { npSetup } from 'ui/new_platform'; +import { PluginsSetup } from 'ui/new_platform/new_platform'; +import { CloudSetup } from '../../../../../plugins/cloud/public'; +import { ajaxErrorHandlersProvider } from './ajax_error_handler'; +import { SetupModeEnterButton } from '../components/setup_mode/enter_button'; + +interface PluginsSetupWithCloud extends PluginsSetup { + cloud: CloudSetup; +} -function isOnPage(hash) { +function isOnPage(hash: string) { return contains(window.location.hash, hash); } -const angularState = { +interface IAngularState { + injector: any; + scope: any; +} + +const angularState: IAngularState = { injector: null, scope: null, }; const checkAngularState = () => { if (!angularState.injector || !angularState.scope) { - throw 'Unable to interact with setup mode because the angular injector was not previously set.' + - ' This needs to be set by calling `initSetupModeState`.'; + throw new Error( + 'Unable to interact with setup mode because the angular injector was not previously set.' + + ' This needs to be set by calling `initSetupModeState`.' + ); } }; -const setupModeState = { +interface ISetupModeState { + enabled: boolean; + data: any; + callbacks: Function[]; +} +const setupModeState: ISetupModeState = { enabled: false, data: null, callbacks: [], @@ -38,7 +56,7 @@ const setupModeState = { export const getSetupModeState = () => setupModeState; -export const setNewlyDiscoveredClusterUuid = clusterUuid => { +export const setNewlyDiscoveredClusterUuid = (clusterUuid: string) => { const globalState = angularState.injector.get('globalState'); const executor = angularState.injector.get('$executor'); angularState.scope.$apply(() => { @@ -48,7 +66,7 @@ export const setNewlyDiscoveredClusterUuid = clusterUuid => { executor.run(); }; -export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) => { +export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid = false) => { checkAngularState(); const http = angularState.injector.get('$http'); @@ -75,19 +93,19 @@ export const fetchCollectionData = async (uuid, fetchWithoutClusterUuid = false) } }; -const notifySetupModeDataChange = oldData => { - setupModeState.callbacks.forEach(cb => cb(oldData)); +const notifySetupModeDataChange = (oldData?: any) => { + setupModeState.callbacks.forEach((cb: Function) => cb(oldData)); }; -export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) => { +export const updateSetupModeData = async (uuid?: string, fetchWithoutClusterUuid = false) => { const oldData = setupModeState.data; const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; - const { cloud } = npSetup.plugins; + const { cloud } = npSetup.plugins as PluginsSetupWithCloud; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const hasPermissions = get(data, '_meta.hasPermissions', false); if (isCloudEnabled || !hasPermissions) { - let text = null; + let text: string = ''; if (!hasPermissions) { text = i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { defaultMessage: 'You do not have the necessary permissions to do this.', @@ -113,9 +131,9 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const globalState = angularState.injector.get('globalState'); const clusterUuid = globalState.cluster_uuid; if (!clusterUuid) { - const liveClusterUuid = get(data, '_meta.liveClusterUuid'); + const liveClusterUuid: string = get(data, '_meta.liveClusterUuid'); const migratedEsNodes = Object.values(get(data, 'elasticsearch.byUuid', {})).filter( - node => node.isPartiallyMigrated || node.isFullyMigrated + (node: any) => node.isPartiallyMigrated || node.isFullyMigrated ); if (liveClusterUuid && migratedEsNodes.length > 0) { setNewlyDiscoveredClusterUuid(liveClusterUuid); @@ -140,7 +158,7 @@ export const disableElasticsearchInternalCollection = async () => { } }; -export const toggleSetupMode = inSetupMode => { +export const toggleSetupMode = (inSetupMode: boolean) => { checkAngularState(); const globalState = angularState.injector.get('globalState'); @@ -164,7 +182,7 @@ export const setSetupModeMenuItem = () => { } const globalState = angularState.injector.get('globalState'); - const { cloud } = npSetup.plugins; + const { cloud } = npSetup.plugins as PluginsSetupWithCloud; const isCloudEnabled = !!(cloud && cloud.isCloudEnabled); const enabled = !globalState.inSetupMode && !isCloudEnabled; @@ -174,10 +192,14 @@ export const setSetupModeMenuItem = () => { ); }; -export const initSetupModeState = async ($scope, $injector, callback) => { +export const addSetupModeCallback = (callback: Function) => setupModeState.callbacks.push(callback); + +export const initSetupModeState = async ($scope: any, $injector: any, callback?: Function) => { angularState.scope = $scope; angularState.injector = $injector; - callback && setupModeState.callbacks.push(callback); + if (callback) { + setupModeState.callbacks.push(callback); + } const globalState = $injector.get('globalState'); if (globalState.inSetupMode) { diff --git a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js index 57a7850b6fd530..1bfc76b7664579 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/alerts/index.js @@ -24,7 +24,7 @@ function getPageData($injector) { const globalState = $injector.get('globalState'); const $http = $injector.get('$http'); const Private = $injector.get('Private'); - const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/alerts`; + const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/legacy_alerts`; const timeBounds = timefilter.getBounds(); diff --git a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js index bec90f3230571e..e7107860d61fa8 100644 --- a/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js +++ b/x-pack/legacy/plugins/monitoring/public/views/cluster/overview/index.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { Fragment } from 'react'; +import { isEmpty } from 'lodash'; +import chrome from 'ui/chrome'; import { i18n } from '@kbn/i18n'; import uiRoutes from 'ui/routes'; import { routeInitProvider } from 'plugins/monitoring/lib/route_init'; @@ -12,7 +14,11 @@ import { MonitoringViewBaseController } from '../../'; import { Overview } from 'plugins/monitoring/components/cluster/overview'; import { I18nContext } from 'ui/i18n'; import { SetupModeRenderer } from '../../../components/renderers'; -import { CODE_PATH_ALL } from '../../../../common/constants'; +import { + CODE_PATH_ALL, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from '../../../../common/constants'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -31,6 +37,7 @@ uiRoutes.when('/overview', { const monitoringClusters = $injector.get('monitoringClusters'); const globalState = $injector.get('globalState'); const showLicenseExpiration = $injector.get('showLicenseExpiration'); + const config = $injector.get('config'); super({ title: i18n.translate('xpack.monitoring.cluster.overviewTitle', { @@ -58,7 +65,16 @@ uiRoutes.when('/overview', { $scope.$watch( () => this.data, - data => { + async data => { + if (isEmpty(data)) { + return; + } + + let emailAddress = chrome.getInjected('monitoringLegacyEmailAddress') || ''; + if (KIBANA_ALERTING_ENABLED) { + emailAddress = config.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS) || emailAddress; + } + this.renderReact( new Promise(resolve => resolve()), + alertInstanceFactory: (id: string) => new AlertInstance(), + savedObjectsClient: {} as jest.Mocked, + }, + params: {}, + state: {}, + spaceId: '', + name: '', + tags: [], + createdBy: null, + updatedBy: null, +}; + +describe('getLicenseExpiration', () => { + const emailAddress = 'foo@foo.com'; + const server: any = { + newPlatform: { + __internals: { + uiSettings: { + asScopedToClient: (): any => ({ + get: () => new Promise(resolve => resolve(emailAddress)), + }), + }, + }, + }, + }; + const getMonitoringCluster: () => void = jest.fn(); + const logger: Logger = { + warn: jest.fn(), + log: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + info: jest.fn(), + get: jest.fn(), + }; + const getLogger = (): Logger => logger; + const ccrEnabled = false; + + afterEach(() => { + (logger.warn as jest.Mock).mockClear(); + }); + + it('should have the right id and actionGroups', () => { + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + expect(alert.id).toBe(ALERT_TYPE_LICENSE_EXPIRATION); + expect(alert.actionGroups).toEqual(['default']); + }); + + it('should return the state if no license is provided', async () => { + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const services: MockServices | AlertServices = { + callCluster: jest.fn(), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + const state = { foo: 1 }; + + const result = await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + }); + + expect(result).toEqual(state); + }); + + it('should log a warning if no email is provided', async () => { + const customServer: any = { + newPlatform: { + __internals: { + uiSettings: { + asScopedToClient: () => ({ + get: () => null, + }), + }, + }, + }, + }; + const alert = getLicenseExpiration(customServer, getMonitoringCluster, getLogger, ccrEnabled); + + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense({ + status: 'good', + type: 'basic', + expiry_date_in_millis: moment() + .add(7, 'days') + .valueOf(), + }) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory: jest.fn(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + + const state = {}; + + await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + }); + + expect((logger.warn as jest.Mock).mock.calls.length).toBe(1); + expect(logger.warn).toHaveBeenCalledWith( + `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` + ); + }); + + it('should fire actions if going to expire', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'gold', + expiry_date_in_millis: moment() + .add(7, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + + expect(newState.expiredCheckDateMS > 0).toBe(true); + expect(scheduleActions.mock.calls.length).toBe(1); + expect(scheduleActions.mock.calls[0][1].subject).toBe( + 'NEW X-Pack Monitoring: License Expiration' + ); + expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); + }); + + it('should fire actions if the user fixed their license', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'gold', + expiry_date_in_millis: moment() + .add(120, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state: AlertState = { + [clusterUuid]: { + expiredCheckDateMS: moment() + .subtract(1, 'day') + .valueOf(), + ui: { isFiring: true, severity: 0, message: null, resolvedMS: 0, expirationTime: 0 }, + }, + }; + + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS).toBe(0); + expect(scheduleActions.mock.calls.length).toBe(1); + expect(scheduleActions.mock.calls[0][1].subject).toBe( + 'RESOLVED X-Pack Monitoring: License Expiration' + ); + expect(scheduleActions.mock.calls[0][1].to).toBe(emailAddress); + }); + + it('should not fire actions for trial license that expire in more than 14 days', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'trial', + expiry_date_in_millis: moment() + .add(15, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS).toBe(undefined); + expect(scheduleActions).not.toHaveBeenCalled(); + }); + + it('should fire actions for trial license that in 14 days or less', async () => { + const scheduleActions = jest.fn(); + const alertInstanceFactory = jest.fn( + (id: string): AlertInstance => { + const instance = new AlertInstance(); + instance.scheduleActions = scheduleActions; + return instance; + } + ); + const alert = getLicenseExpiration(server, getMonitoringCluster, getLogger, ccrEnabled); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockReturnValue( + new Promise(resolve => { + const savedObject: SavedObject = { + id: '', + type: '', + references: [], + attributes: { + [MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS]: emailAddress, + }, + }; + resolve(savedObject); + }) + ); + const services = { + callCluster: jest.fn( + (method: string, { filterPath }): Promise => { + return new Promise(resolve => { + if (filterPath.includes('hits.hits._source.license.*')) { + resolve( + fillLicense( + { + status: 'active', + type: 'trial', + expiry_date_in_millis: moment() + .add(13, 'days') + .valueOf(), + }, + clusterUuid + ) + ); + } + resolve({}); + }); + } + ), + alertInstanceFactory, + savedObjectsClient, + }; + + const state = {}; + const result: AlertState = (await alert.executor({ + ...alertExecutorOptions, + services, + params, + state, + })) as AlertState; + + const newState: AlertClusterState = result[clusterUuid] as AlertClusterState; + expect(newState.expiredCheckDateMS > 0).toBe(true); + expect(scheduleActions.mock.calls.length).toBe(1); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts new file mode 100644 index 00000000000000..197c5c9cdcbc72 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/license_expiration.ts @@ -0,0 +1,162 @@ +/* + * 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 moment from 'moment-timezone'; +import { get } from 'lodash'; +import { Legacy } from 'kibana'; +import { Logger } from 'src/core/server'; +import { ALERT_TYPE_LICENSE_EXPIRATION, INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; +import { AlertType } from '../../../alerting'; +import { fetchLicenses } from '../lib/alerts/fetch_licenses'; +import { fetchDefaultEmailAddress } from '../lib/alerts/fetch_default_email_address'; +import { fetchClusters } from '../lib/alerts/fetch_clusters'; +import { fetchAvailableCcs } from '../lib/alerts/fetch_available_ccs'; +import { + AlertLicense, + AlertState, + AlertClusterState, + AlertClusterUiState, + LicenseExpirationAlertExecutorOptions, +} from './types'; +import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; +import { executeActions, getUiMessage } from '../lib/alerts/license_expiration.lib'; + +const EXPIRES_DAYS = [60, 30, 14, 7]; + +export const getLicenseExpiration = ( + server: Legacy.Server, + getMonitoringCluster: any, + getLogger: (contexts: string[]) => Logger, + ccsEnabled: boolean +): AlertType => { + async function getCallCluster(services: any): Promise { + const monitoringCluster = await getMonitoringCluster(); + if (!monitoringCluster) { + return services.callCluster; + } + + return monitoringCluster.callCluster; + } + + const logger = getLogger([ALERT_TYPE_LICENSE_EXPIRATION]); + return { + id: ALERT_TYPE_LICENSE_EXPIRATION, + name: 'Monitoring Alert - License Expiration', + actionGroups: ['default'], + async executor({ + services, + params, + state, + }: LicenseExpirationAlertExecutorOptions): Promise { + logger.debug( + `Firing alert with params: ${JSON.stringify(params)} and state: ${JSON.stringify(state)}` + ); + + const callCluster = await getCallCluster(services); + + // Support CCS use cases by querying to find available remote clusters + // and then adding those to the index pattern we are searching against + let esIndexPattern = INDEX_PATTERN_ELASTICSEARCH; + if (ccsEnabled) { + const availableCcs = await fetchAvailableCcs(callCluster); + if (availableCcs.length > 0) { + esIndexPattern = getCcsIndexPattern(esIndexPattern, availableCcs); + } + } + + const clusters = await fetchClusters(callCluster, esIndexPattern); + + // Fetch licensing information from cluster_stats documents + const licenses: AlertLicense[] = await fetchLicenses(callCluster, clusters, esIndexPattern); + if (licenses.length === 0) { + logger.warn(`No license found for ${ALERT_TYPE_LICENSE_EXPIRATION}.`); + return state; + } + + const uiSettings = server.newPlatform.__internals.uiSettings.asScopedToClient( + services.savedObjectsClient + ); + const dateFormat: string = await uiSettings.get('dateFormat'); + const timezone: string = await uiSettings.get('dateFormat:tz'); + const emailAddress = await fetchDefaultEmailAddress(uiSettings); + if (!emailAddress) { + // TODO: we can do more here + logger.warn( + `Unable to send email for ${ALERT_TYPE_LICENSE_EXPIRATION} because there is no email configured.` + ); + return; + } + + const result: AlertState = { ...state }; + + for (const license of licenses) { + const licenseState: AlertClusterState = state[license.clusterUuid] || {}; + const $expiry = moment.utc(license.expiryDateMS); + let isExpired = false; + let severity = 0; + + if (license.status !== 'active') { + isExpired = true; + severity = 2001; + } else if (license.expiryDateMS) { + for (let i = EXPIRES_DAYS.length - 1; i >= 0; i--) { + if (license.type === 'trial' && i < 2) { + break; + } + + const $fromNow = moment.utc().add(EXPIRES_DAYS[i], 'days'); + if ($fromNow.isAfter($expiry)) { + isExpired = true; + severity = 1000 * i; + break; + } + } + } + + const ui: AlertClusterUiState = get(licenseState, 'ui', { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + expirationTime: 0, + }); + let resolved = ui.resolvedMS; + let message = ui.message; + let expiredCheckDate = licenseState.expiredCheckDateMS; + const instance = services.alertInstanceFactory(ALERT_TYPE_LICENSE_EXPIRATION); + + if (isExpired) { + if (!licenseState.expiredCheckDateMS) { + logger.debug(`License will expire soon, sending email`); + executeActions(instance, license, $expiry, dateFormat, emailAddress); + expiredCheckDate = moment().valueOf(); + } + message = getUiMessage(license, timezone); + resolved = 0; + } else if (!isExpired && licenseState.expiredCheckDateMS) { + logger.debug(`License expiration has been resolved, sending email`); + executeActions(instance, license, $expiry, dateFormat, emailAddress, true); + expiredCheckDate = 0; + message = getUiMessage(license, timezone, true); + resolved = moment().valueOf(); + } + + result[license.clusterUuid] = { + expiredCheckDateMS: expiredCheckDate, + ui: { + message, + expirationTime: license.expiryDateMS, + isFiring: expiredCheckDate > 0, + severity, + resolvedMS: resolved, + }, + }; + } + + return result; + }, + }; +}; diff --git a/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts b/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts new file mode 100644 index 00000000000000..6346ca00dabbd2 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/alerts/types.d.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Moment } from 'moment'; +import { AlertExecutorOptions } from '../../../alerting'; + +export interface AlertLicense { + status: string; + type: string; + expiryDateMS: number; + clusterUuid: string; + clusterName: string; +} + +export interface AlertState { + [clusterUuid: string]: AlertClusterState; +} + +export interface AlertClusterState { + expiredCheckDateMS: number | Moment; + ui: AlertClusterUiState; +} + +export interface AlertClusterUiState { + isFiring: boolean; + severity: number; + message: string | null; + resolvedMS: number; + expirationTime: number; +} + +export interface AlertCluster { + clusterUuid: string; +} + +export interface LicenseExpirationAlertExecutorOptions extends AlertExecutorOptions { + state: AlertState; +} + +export interface AlertParams { + dateFormat: string; + timezone: string; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts new file mode 100644 index 00000000000000..4398b2dd675ec3 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { fetchAvailableCcs } from './fetch_available_ccs'; + +describe('fetchAvailableCcs', () => { + it('should call the `cluster.remoteInfo` api', async () => { + const callCluster = jest.fn(); + await fetchAvailableCcs(callCluster); + expect(callCluster).toHaveBeenCalledWith('cluster.remoteInfo'); + }); + + it('should return clusters that are connected', async () => { + const connectedRemote = 'myRemote'; + const callCluster = jest.fn().mockImplementation(() => ({ + [connectedRemote]: { + connected: true, + }, + })); + const result = await fetchAvailableCcs(callCluster); + expect(result).toEqual([connectedRemote]); + }); + + it('should not return clusters that are connected', async () => { + const disconnectedRemote = 'myRemote'; + const callCluster = jest.fn().mockImplementation(() => ({ + [disconnectedRemote]: { + connected: false, + }, + })); + const result = await fetchAvailableCcs(callCluster); + expect(result.length).toBe(0); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts new file mode 100644 index 00000000000000..34efaff93f34c7 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_available_ccs.ts @@ -0,0 +1,19 @@ +/* + * 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 async function fetchAvailableCcs(callCluster: any): Promise { + const availableCcs = []; + const response = await callCluster('cluster.remoteInfo'); + for (const remoteName in response) { + if (!response.hasOwnProperty(remoteName)) { + continue; + } + const remoteInfo = response[remoteName]; + if (remoteInfo.connected) { + availableCcs.push(remoteName); + } + } + return availableCcs; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts new file mode 100644 index 00000000000000..78eb9773df15fb --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { fetchClusters } from './fetch_clusters'; + +describe('fetchClusters', () => { + it('return a list of clusters', async () => { + const callCluster = jest.fn().mockImplementation(() => ({ + aggregations: { + clusters: { + buckets: [ + { + key: 'clusterA', + }, + ], + }, + }, + })); + const index = '.monitoring-es-*'; + const result = await fetchClusters(callCluster, index); + expect(result).toEqual([{ clusterUuid: 'clusterA' }]); + }); + + it('should limit the time period in the query', async () => { + const callCluster = jest.fn(); + const index = '.monitoring-es-*'; + await fetchClusters(callCluster, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[1].range.timestamp.gte).toBe('now-2m'); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts new file mode 100644 index 00000000000000..8ef7339618a2c0 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_clusters.ts @@ -0,0 +1,52 @@ +/* + * 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 { get } from 'lodash'; +import { AlertCluster } from '../../alerts/types'; + +interface AggregationResult { + key: string; +} + +export async function fetchClusters(callCluster: any, index: string): Promise { + const params = { + index, + filterPath: 'aggregations.clusters.buckets', + body: { + size: 0, + query: { + bool: { + filter: [ + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + aggs: { + clusters: { + terms: { + field: 'cluster_uuid', + size: 1000, + }, + }, + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'aggregations.clusters.buckets', []).map((bucket: AggregationResult) => ({ + clusterUuid: bucket.key, + })); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts new file mode 100644 index 00000000000000..25b09b956038a0 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.test.ts @@ -0,0 +1,17 @@ +/* + * 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 { fetchDefaultEmailAddress } from './fetch_default_email_address'; +import { uiSettingsServiceMock } from '../../../../../../../src/core/server/mocks'; + +describe('fetchDefaultEmailAddress', () => { + it('get the email address', async () => { + const email = 'test@test.com'; + const uiSettingsClient = uiSettingsServiceMock.createClient(); + uiSettingsClient.get.mockResolvedValue(email); + const result = await fetchDefaultEmailAddress(uiSettingsClient); + expect(result).toBe(email); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts new file mode 100644 index 00000000000000..88e4199a88256a --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_default_email_address.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IUiSettingsClient } from 'src/core/server'; +import { MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS } from '../../../common/constants'; + +export async function fetchDefaultEmailAddress( + uiSettingsClient: IUiSettingsClient +): Promise { + return await uiSettingsClient.get(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts new file mode 100644 index 00000000000000..dd6c074e68b1f9 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { fetchLicenses } from './fetch_licenses'; + +describe('fetchLicenses', () => { + it('return a list of licenses', async () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_name: clusterName, + cluster_uuid: clusterUuid, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + clusterName, + }, + ]); + }); + + it('should only search for the clusters provided', async () => { + const clusterUuid = 'clusterA'; + const callCluster = jest.fn(); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[0].terms.cluster_uuid).toEqual([clusterUuid]); + }); + + it('should limit the time period in the query', async () => { + const clusterUuid = 'clusterA'; + const callCluster = jest.fn(); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + await fetchLicenses(callCluster, clusters, index); + const params = callCluster.mock.calls[0][1]; + expect(params.body.query.bool.filter[2].range.timestamp.gte).toBe('now-2m'); + }); + + it('should give priority to the metadata name', async () => { + const clusterName = 'MyCluster'; + const clusterUuid = 'clusterA'; + const license = { + status: 'active', + expiry_date_in_millis: 1579532493876, + type: 'basic', + }; + const callCluster = jest.fn().mockImplementation(() => ({ + hits: { + hits: [ + { + _source: { + license, + cluster_name: 'fakeName', + cluster_uuid: clusterUuid, + cluster_settings: { + cluster: { + metadata: { + display_name: clusterName, + }, + }, + }, + }, + }, + ], + }, + })); + const clusters = [{ clusterUuid }]; + const index = '.monitoring-es-*'; + const result = await fetchLicenses(callCluster, clusters, index); + expect(result).toEqual([ + { + status: license.status, + type: license.type, + expiryDateMS: license.expiry_date_in_millis, + clusterUuid, + clusterName, + }, + ]); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts new file mode 100644 index 00000000000000..31a68e8aa9c3ee --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_licenses.ts @@ -0,0 +1,67 @@ +/* + * 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 { get } from 'lodash'; +import { AlertLicense, AlertCluster } from '../../alerts/types'; + +export async function fetchLicenses( + callCluster: any, + clusters: AlertCluster[], + index: string +): Promise { + const params = { + index, + filterPath: [ + 'hits.hits._source.license.*', + 'hits.hits._source.cluster_settings.cluster.metadata.display_name', + 'hits.hits._source.cluster_uuid', + 'hits.hits._source.cluster_name', + ], + body: { + size: 1, + sort: [{ timestamp: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + terms: { + cluster_uuid: clusters.map(cluster => cluster.clusterUuid), + }, + }, + { + term: { + type: 'cluster_stats', + }, + }, + { + range: { + timestamp: { + gte: 'now-2m', + }, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + return get(response, 'hits.hits', []).map((hit: any) => { + const clusterName: string = + get(hit, '_source.cluster_settings.cluster.metadata.display_name') || + get(hit, '_source.cluster_name') || + get(hit, '_source.cluster_uuid'); + const rawLicense: any = get(hit, '_source.license', {}); + const license: AlertLicense = { + status: rawLicense.status, + type: rawLicense.type, + expiryDateMS: rawLicense.expiry_date_in_millis, + clusterUuid: get(hit, '_source.cluster_uuid'), + clusterName, + }; + return license; + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts new file mode 100644 index 00000000000000..9f7c1d5a994d27 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -0,0 +1,87 @@ +/* + * 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 moment from 'moment'; +import { get } from 'lodash'; +import { AlertClusterState } from '../../alerts/types'; +import { ALERT_TYPES, LOGGING_TAG } from '../../../common/constants'; + +export async function fetchStatus( + callCluster: any, + start: number, + end: number, + clusterUuid: string, + server: any +): Promise { + // TODO: this shouldn't query task manager directly but rather + // use an api exposed by the alerting/actions plugin + // See https://github.com/elastic/kibana/issues/48442 + const statuses = await Promise.all( + ALERT_TYPES.map( + type => + new Promise(async (resolve, reject) => { + try { + const params = { + index: '.kibana_task_manager', + filterPath: ['hits.hits._source.task.state'], + body: { + size: 1, + sort: [{ updated_at: { order: 'desc' } }], + query: { + bool: { + filter: [ + { + term: { + 'task.taskType': `alerting:${type}`, + }, + }, + ], + }, + }, + }, + }; + + const response = await callCluster('search', params); + const state = get(response, 'hits.hits[0]._source.task.state', '{}'); + const clusterState: AlertClusterState = get( + JSON.parse(state), + `alertTypeState.${clusterUuid}`, + { + expiredCheckDateMS: 0, + ui: { + isFiring: false, + message: null, + severity: 0, + resolvedMS: 0, + expirationTime: 0, + }, + } + ); + const isInBetween = moment(clusterState.ui.resolvedMS).isBetween(start, end); + if (clusterState.ui.isFiring || isInBetween) { + return resolve({ + type, + ...clusterState.ui, + }); + } + return resolve(false); + } catch (err) { + const reason = get(err, 'body.error.type'); + if (reason === 'index_not_found_exception') { + server.log( + ['error', LOGGING_TAG], + `Unable to fetch alerts. Alerts depends on task manager, which has not been started yet.` + ); + } else { + server.log(['error', LOGGING_TAG], err.message); + } + return resolve(false); + } + }) + ) + ); + + return statuses.filter(Boolean); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.ts new file mode 100644 index 00000000000000..a5eb1049861612 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.test.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 { getCcsIndexPattern } from './get_ccs_index_pattern'; + +describe('getCcsIndexPattern', () => { + it('should return an index pattern including remotes', () => { + const remotes = ['Remote1', 'Remote2']; + const index = '.monitoring-es-*'; + const result = getCcsIndexPattern(index, remotes); + expect(result).toBe('.monitoring-es-*,Remote1:.monitoring-es-*,Remote2:.monitoring-es-*'); + }); + + it('should return an index pattern from multiple index patterns including remotes', () => { + const remotes = ['Remote1', 'Remote2']; + const index = '.monitoring-es-*,.monitoring-kibana-*'; + const result = getCcsIndexPattern(index, remotes); + expect(result).toBe( + '.monitoring-es-*,.monitoring-kibana-*,Remote1:.monitoring-es-*,Remote2:.monitoring-es-*,Remote1:.monitoring-kibana-*,Remote2:.monitoring-kibana-*' + ); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts new file mode 100644 index 00000000000000..b562fde2a0810f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/get_ccs_index_pattern.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +export function getCcsIndexPattern(indexPattern: string, remotes: string[]): string { + return `${indexPattern},${indexPattern + .split(',') + .map(pattern => { + return remotes.map(remoteName => `${remoteName}:${pattern}`).join(','); + }) + .join(',')}`; +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts new file mode 100644 index 00000000000000..1a2eb1e44be843 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.test.ts @@ -0,0 +1,55 @@ +/* + * 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 moment from 'moment-timezone'; +import { executeActions, getUiMessage } from './license_expiration.lib'; + +describe('licenseExpiration lib', () => { + describe('executeActions', () => { + const clusterName = 'clusterA'; + const instance: any = { scheduleActions: jest.fn() }; + const license: any = { clusterName }; + const $expiry = moment('2020-01-20'); + const dateFormat = 'dddd, MMMM Do YYYY, h:mm:ss a'; + const emailAddress = 'test@test.com'; + + beforeEach(() => { + instance.scheduleActions.mockClear(); + }); + + it('should schedule actions when firing', () => { + executeActions(instance, license, $expiry, dateFormat, emailAddress, false); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'NEW X-Pack Monitoring: License Expiration', + message: `Cluster '${clusterName}' license is going to expire on Monday, January 20th 2020, 12:00:00 am. Please update your license.`, + to: emailAddress, + }); + }); + + it('should schedule actions when resolved', () => { + executeActions(instance, license, $expiry, dateFormat, emailAddress, true); + expect(instance.scheduleActions).toHaveBeenCalledWith('default', { + subject: 'RESOLVED X-Pack Monitoring: License Expiration', + message: `This cluster alert has been resolved: Cluster '${clusterName}' license was going to expire on Monday, January 20th 2020, 12:00:00 am.`, + to: emailAddress, + }); + }); + }); + + describe('getUiMessage', () => { + const timezone = 'Europe/London'; + const license: any = { expiryDateMS: moment.tz('2020-01-20 08:00:00', timezone).utc() }; + + it('should return a message when firing', () => { + const message = getUiMessage(license, timezone, false); + expect(message).toBe(`This cluster's license is going to expire in #relative at #absolute.`); + }); + + it('should return a message when resolved', () => { + const message = getUiMessage(license, timezone, true); + expect(message).toBe(`This cluster's license is active.`); + }); + }); +}); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts new file mode 100644 index 00000000000000..8a75fc1fbbd82f --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/alerts/license_expiration.lib.ts @@ -0,0 +1,58 @@ +/* + * 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 { Moment } from 'moment-timezone'; +import { i18n } from '@kbn/i18n'; +import { AlertInstance } from '../../../../alerting/server/alert_instance'; +import { AlertLicense } from '../../alerts/types'; + +const RESOLVED_SUBJECT = i18n.translate( + 'xpack.monitoring.alerts.licenseExpiration.resolvedSubject', + { + defaultMessage: 'RESOLVED X-Pack Monitoring: License Expiration', + } +); + +const NEW_SUBJECT = i18n.translate('xpack.monitoring.alerts.licenseExpiration.newSubject', { + defaultMessage: 'NEW X-Pack Monitoring: License Expiration', +}); + +export function executeActions( + instance: AlertInstance, + license: AlertLicense, + $expiry: Moment, + dateFormat: string, + emailAddress: string, + resolved: boolean = false +) { + if (resolved) { + instance.scheduleActions('default', { + subject: RESOLVED_SUBJECT, + message: `This cluster alert has been resolved: Cluster '${ + license.clusterName + }' license was going to expire on ${$expiry.format(dateFormat)}.`, + to: emailAddress, + }); + } else { + instance.scheduleActions('default', { + subject: NEW_SUBJECT, + message: `Cluster '${license.clusterName}' license is going to expire on ${$expiry.format( + dateFormat + )}. Please update your license.`, + to: emailAddress, + }); + } +} + +export function getUiMessage(license: AlertLicense, timezone: string, resolved: boolean = false) { + if (resolved) { + return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage', { + defaultMessage: `This cluster's license is active.`, + }); + } + return i18n.translate('xpack.monitoring.alerts.licenseExpiration.ui.firingMessage', { + defaultMessage: `This cluster's license is going to expire in #relative at #absolute.`, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 2b080a5c333fcf..a5426dc04545eb 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -16,6 +16,7 @@ import { getBeatsForClusters } from '../beats'; import { alertsClustersAggregation } from '../../cluster_alerts/alerts_clusters_aggregation'; import { alertsClusterSearch } from '../../cluster_alerts/alerts_cluster_search'; import { checkLicense as checkLicenseForAlerts } from '../../cluster_alerts/check_license'; +import { fetchStatus } from '../alerts/fetch_status'; import { getClustersSummary } from './get_clusters_summary'; import { CLUSTER_ALERTS_SEARCH_SIZE, @@ -27,6 +28,7 @@ import { CODE_PATH_LOGSTASH, CODE_PATH_BEATS, CODE_PATH_APM, + KIBANA_ALERTING_ENABLED, } from '../../../common/constants'; import { getApmsForClusters } from '../apm/get_apms_for_clusters'; import { i18n } from '@kbn/i18n'; @@ -99,15 +101,31 @@ export async function getClustersFromRequest( if (mlJobs !== null) { cluster.ml = { jobs: mlJobs }; } - const alerts = isInCodePath(codePaths, [CODE_PATH_ALERTS]) - ? await alertsClusterSearch(req, alertsIndex, cluster, checkLicenseForAlerts, { + + if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { + if (KIBANA_ALERTING_ENABLED) { + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + const callCluster = (...args) => callWithRequest(req, ...args); + cluster.alerts = await fetchStatus( + callCluster, start, end, - size: CLUSTER_ALERTS_SEARCH_SIZE, - }) - : null; - if (alerts) { - cluster.alerts = alerts; + cluster.cluster_uuid, + req.server + ); + } else { + cluster.alerts = await alertsClusterSearch( + req, + alertsIndex, + cluster, + checkLicenseForAlerts, + { + start, + end, + size: CLUSTER_ALERTS_SEARCH_SIZE, + } + ); + } } cluster.logs = isInCodePath(codePaths, [CODE_PATH_LOGS]) diff --git a/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js b/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js new file mode 100644 index 00000000000000..89cbf20d9b56f7 --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/lib/get_date_format.js @@ -0,0 +1,9 @@ +/* + * 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 async function getDateFormat(req) { + return await req.getUiSettingsService().get('dateFormat'); +} diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index 5f52e0c6a983b7..a12b48510a6ff7 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -348,7 +348,6 @@ export const getCollectionStatus = async ( }, }; } - const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; diff --git a/x-pack/legacy/plugins/monitoring/server/plugin.js b/x-pack/legacy/plugins/monitoring/server/plugin.js index ef346e95ad0757..50e5319a0f5263 100644 --- a/x-pack/legacy/plugins/monitoring/server/plugin.js +++ b/x-pack/legacy/plugins/monitoring/server/plugin.js @@ -5,12 +5,17 @@ */ import { i18n } from '@kbn/i18n'; -import { LOGGING_TAG, KIBANA_MONITORING_LOGGING_TAG } from '../common/constants'; +import { + LOGGING_TAG, + KIBANA_MONITORING_LOGGING_TAG, + KIBANA_ALERTING_ENABLED, +} from '../common/constants'; import { requireUIRoutes } from './routes'; import { instantiateClient } from './es_client/instantiate_client'; import { initMonitoringXpackInfo } from './init_monitoring_xpack_info'; import { initBulkUploader, registerCollectors } from './kibana_monitoring'; import { registerMonitoringCollection } from './telemetry_collection'; +import { getLicenseExpiration } from './alerts/license_expiration'; import { parseElasticsearchConfig } from './es_client/parse_elasticsearch_config'; export class Plugin { @@ -133,5 +138,37 @@ export class Plugin { showCgroupMetricsLogstash: config.get('monitoring.ui.container.logstash.enabled'), // Note, not currently used, but see https://github.com/elastic/x-pack-kibana/issues/1559 part 2 }; }); + + if (KIBANA_ALERTING_ENABLED && plugins.alerting) { + // this is not ready right away but we need to register alerts right away + async function getMonitoringCluster() { + const configs = config.get('xpack.monitoring.elasticsearch'); + if (configs.hosts) { + const monitoringCluster = plugins.elasticsearch.getCluster('monitoring'); + const { username, password } = configs; + const fakeRequest = { + headers: { + authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, + }, + }; + return { + callCluster: (...args) => monitoringCluster.callWithRequest(fakeRequest, ...args), + }; + } + return null; + } + + function getLogger(contexts) { + return core.logger.get('plugins', LOGGING_TAG, ...contexts); + } + plugins.alerting.setup.registerType( + getLicenseExpiration( + core._hapi, + getMonitoringCluster, + getLogger, + config.get('xpack.monitoring.ccs.enabled') + ) + ); + } } } diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js new file mode 100644 index 00000000000000..f87683effe437c --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/alerts.js @@ -0,0 +1,89 @@ +/* + * 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 Joi from 'joi'; +import { isFunction } from 'lodash'; +import { + ALERT_TYPE_LICENSE_EXPIRATION, + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, +} from '../../../../../common/constants'; + +async function createAlerts(req, alertsClient, { selectedEmailActionId }) { + const createdAlerts = []; + + // Create alerts + const ALERT_TYPES = { + [ALERT_TYPE_LICENSE_EXPIRATION]: { + schedule: { interval: '10s' }, + actions: [ + { + group: 'default', + id: selectedEmailActionId, + params: { + subject: '{{context.subject}}', + message: `{{context.message}}`, + to: ['{{context.to}}'], + }, + }, + ], + }, + }; + + for (const alertTypeId of Object.keys(ALERT_TYPES)) { + const existingAlert = await alertsClient.find({ + options: { + search: alertTypeId, + }, + }); + if (existingAlert.total === 1) { + await alertsClient.delete({ id: existingAlert.data[0].id }); + } + + const result = await alertsClient.create({ + data: { + enabled: true, + alertTypeId, + ...ALERT_TYPES[alertTypeId], + }, + }); + createdAlerts.push(result); + } + + return createdAlerts; +} + +async function saveEmailAddress(emailAddress, uiSettingsService) { + await uiSettingsService.set(MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, emailAddress); +} + +export function createKibanaAlertsRoute(server) { + server.route({ + method: 'POST', + path: '/api/monitoring/v1/alerts', + config: { + validate: { + payload: Joi.object({ + selectedEmailActionId: Joi.string().required(), + emailAddress: Joi.string().required(), + }), + }, + }, + async handler(req, headers) { + const { emailAddress, selectedEmailActionId } = req.payload; + const alertsClient = isFunction(req.getAlertsClient) ? req.getAlertsClient() : null; + if (!alertsClient) { + return headers.response().code(404); + } + + const [alerts, emailResponse] = await Promise.all([ + createAlerts(req, alertsClient, { ...req.params, selectedEmailActionId }), + saveEmailAddress(emailAddress, req.getUiSettingsService()), + ]); + + return { alerts, emailResponse }; + }, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js index cdcd776b349fc6..246cdfde97cff8 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/index.js @@ -4,54 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import Joi from 'joi'; -import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; -import { checkLicense } from '../../../../cluster_alerts/check_license'; -import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; -import { prefixIndexPattern } from '../../../../lib/ccs_utils'; -import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; - -/* - * Cluster Alerts route. - */ -export function clusterAlertsRoute(server) { - server.route({ - method: 'POST', - path: '/api/monitoring/v1/clusters/{clusterUuid}/alerts', - config: { - validate: { - params: Joi.object({ - clusterUuid: Joi.string().required(), - }), - payload: Joi.object({ - ccs: Joi.string().optional(), - timeRange: Joi.object({ - min: Joi.date().required(), - max: Joi.date().required(), - }).required(), - }), - }, - }, - handler(req) { - const config = server.config(); - const ccs = req.payload.ccs; - const clusterUuid = req.params.clusterUuid; - const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); - const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); - const options = { - start: req.payload.timeRange.min, - end: req.payload.timeRange.max, - }; - - return getClusterLicense(req, esIndexPattern, clusterUuid).then(license => - alertsClusterSearch( - req, - alertsIndex, - { cluster_uuid: clusterUuid, license }, - checkLicense, - options - ) - ); - }, - }); -} +export * from './legacy_alerts'; +export * from './alerts'; diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js new file mode 100644 index 00000000000000..a3049f0f3e2d2e --- /dev/null +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/alerts/legacy_alerts.js @@ -0,0 +1,57 @@ +/* + * 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 Joi from 'joi'; +import { alertsClusterSearch } from '../../../../cluster_alerts/alerts_cluster_search'; +import { checkLicense } from '../../../../cluster_alerts/check_license'; +import { getClusterLicense } from '../../../../lib/cluster/get_cluster_license'; +import { prefixIndexPattern } from '../../../../lib/ccs_utils'; +import { INDEX_PATTERN_ELASTICSEARCH, INDEX_ALERTS } from '../../../../../common/constants'; + +/* + * Cluster Alerts route. + */ +export function legacyClusterAlertsRoute(server) { + server.route({ + method: 'POST', + path: '/api/monitoring/v1/clusters/{clusterUuid}/legacy_alerts', + config: { + validate: { + params: Joi.object({ + clusterUuid: Joi.string().required(), + }), + payload: Joi.object({ + ccs: Joi.string().optional(), + timeRange: Joi.object({ + min: Joi.date().required(), + max: Joi.date().required(), + }).required(), + }), + }, + }, + handler(req) { + const config = server.config(); + const ccs = req.payload.ccs; + const clusterUuid = req.params.clusterUuid; + const esIndexPattern = prefixIndexPattern(config, INDEX_PATTERN_ELASTICSEARCH, ccs); + const alertsIndex = prefixIndexPattern(config, INDEX_ALERTS, ccs); + const options = { + start: req.payload.timeRange.min, + end: req.payload.timeRange.max, + }; + + return getClusterLicense(req, esIndexPattern, clusterUuid).then(license => + alertsClusterSearch( + req, + alertsIndex, + { cluster_uuid: clusterUuid, license }, + checkLicense, + options + ) + ); + }, + }); +} diff --git a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js index baffbfd5f3f6f2..de0213ec84689d 100644 --- a/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js +++ b/x-pack/legacy/plugins/monitoring/server/routes/api/v1/ui.js @@ -6,7 +6,7 @@ // all routes for the app export { checkAccessRoute } from './check_access'; -export { clusterAlertsRoute } from './alerts/'; +export * from './alerts/'; export { beatsDetailRoute, beatsListingRoute, beatsOverviewRoute } from './beats'; export { clusterRoute, clustersRoute } from './cluster'; export { diff --git a/x-pack/legacy/plugins/monitoring/ui_exports.js b/x-pack/legacy/plugins/monitoring/ui_exports.js index 9251deb673bd11..49f167b0f1b103 100644 --- a/x-pack/legacy/plugins/monitoring/ui_exports.js +++ b/x-pack/legacy/plugins/monitoring/ui_exports.js @@ -6,6 +6,10 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; +import { + MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS, + KIBANA_ALERTING_ENABLED, +} from './common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; /** @@ -14,28 +18,48 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; * app (injectDefaultVars and hacks) * @return {Object} data per Kibana plugin uiExport schema */ -export const getUiExports = () => ({ - app: { - title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { - defaultMessage: 'Stack Monitoring', - }), - order: 9002, - description: i18n.translate('xpack.monitoring.uiExportsDescription', { - defaultMessage: 'Monitoring for Elastic Stack', - }), - icon: 'plugins/monitoring/icons/monitoring.svg', - euiIconType: 'monitoringApp', - linkToLastSubUrl: false, - main: 'plugins/monitoring/monitoring', - category: DEFAULT_APP_CATEGORIES.management, - }, - injectDefaultVars(server) { - const config = server.config(); - return { - monitoringUiEnabled: config.get('monitoring.ui.enabled'), +export const getUiExports = () => { + const uiSettingDefaults = {}; + if (KIBANA_ALERTING_ENABLED) { + uiSettingDefaults[MONITORING_CONFIG_ALERTING_EMAIL_ADDRESS] = { + name: i18n.translate('xpack.monitoring.alertingEmailAddress.name', { + defaultMessage: 'Alerting email address', + }), + value: '', + description: i18n.translate('xpack.monitoring.alertingEmailAddress.description', { + defaultMessage: `The default email address to receive alerts from Stack Monitoring`, + }), + category: ['monitoring'], }; - }, - hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], - home: ['plugins/monitoring/register_feature'], - styleSheetPaths: resolve(__dirname, 'public/index.scss'), -}); + } + + return { + app: { + title: i18n.translate('xpack.monitoring.stackMonitoringTitle', { + defaultMessage: 'Stack Monitoring', + }), + order: 9002, + description: i18n.translate('xpack.monitoring.uiExportsDescription', { + defaultMessage: 'Monitoring for Elastic Stack', + }), + icon: 'plugins/monitoring/icons/monitoring.svg', + euiIconType: 'monitoringApp', + linkToLastSubUrl: false, + main: 'plugins/monitoring/monitoring', + category: DEFAULT_APP_CATEGORIES.management, + }, + injectDefaultVars(server) { + const config = server.config(); + return { + monitoringUiEnabled: config.get('monitoring.ui.enabled'), + monitoringLegacyEmailAddress: config.get( + 'monitoring.cluster_alerts.email_notifications.email_address' + ), + }; + }, + uiSettingDefaults, + hacks: ['plugins/monitoring/hacks/toggle_app_link_in_nav'], + home: ['plugins/monitoring/register_feature'], + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + }; +}; diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 784125b83859d4..fbd7404a2f15eb 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -9,3 +9,10 @@ export interface ActionType { name: string; enabled: boolean; } + +export interface ActionResult { + id: string; + actionTypeId: string; + name: string; + config: Record; +} From d5ba32ff085cbbe4f9e0478ba261a6a2d2af129c Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 3 Feb 2020 13:11:48 -0800 Subject: [PATCH 52/62] [APM] Fixes maxTraceItems in waterfall trace and related error queries (#56111) * Addresses #55494 by combining waterfall trace and related error queries. Makes sure that maxTraceItems is only respected for Transactions and Spans. * addressed feedback back separating the combined query into 1 for transactions/spans and 1 for errors + errorCount Co-authored-by: Elastic Machine --- .../Marks/__test__/get_error_marks.test.ts | 15 +- .../Marks/get_error_marks.ts | 9 +- .../WaterfallContainer/Waterfall/index.tsx | 6 +- .../waterfall_helpers.test.ts.snap | 141 +++++++++--------- .../waterfall_helpers.test.ts | 17 ++- .../waterfall_helpers/waterfall_helpers.ts | 91 +++++++++-- .../plugins/apm/public/hooks/useWaterfall.ts | 2 +- .../errors/__snapshots__/queries.test.ts.snap | 64 -------- .../get_trace_errors_per_transaction.ts | 65 -------- .../apm/server/lib/errors/queries.test.ts | 9 -- .../traces/__snapshots__/queries.test.ts.snap | 50 +++---- .../apm/server/lib/traces/get_trace.ts | 9 +- .../apm/server/lib/traces/get_trace_items.ts | 80 +++++++++- x-pack/legacy/plugins/apm/typings/common.d.ts | 4 + 14 files changed, 267 insertions(+), 295 deletions(-) delete mode 100644 x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts index 8fd8edd7f8a728..b7e83073a205b1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/__test__/get_error_marks.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IWaterfallItem } from '../../Waterfall/waterfall_helpers/waterfall_helpers'; +import { IWaterfallError } from '../../Waterfall/waterfall_helpers/waterfall_helpers'; import { getErrorMarks } from '../get_error_marks'; describe('getErrorMarks', () => { @@ -12,13 +12,6 @@ describe('getErrorMarks', () => { it('when items are missing', () => { expect(getErrorMarks([], {})).toEqual([]); }); - it('when any error is available', () => { - const items = [ - { docType: 'span' }, - { docType: 'transaction' } - ] as IWaterfallItem[]; - expect(getErrorMarks(items, {})).toEqual([]); - }); }); it('returns error marks', () => { @@ -29,14 +22,13 @@ describe('getErrorMarks', () => { skew: 5, doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } } as unknown, - { docType: 'transaction' }, { docType: 'error', offset: 50, skew: 0, doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } } as unknown - ] as IWaterfallItem[]; + ] as IWaterfallError[]; expect( getErrorMarks(items, { 'opbeans-java': 'red', 'opbeans-node': 'blue' }) ).toEqual([ @@ -67,14 +59,13 @@ describe('getErrorMarks', () => { skew: 5, doc: { error: { id: 1 }, service: { name: 'opbeans-java' } } } as unknown, - { docType: 'transaction' }, { docType: 'error', offset: 50, skew: 0, doc: { error: { id: 2 }, service: { name: 'opbeans-node' } } } as unknown - ] as IWaterfallItem[]; + ] as IWaterfallError[]; expect(getErrorMarks(items, {})).toEqual([ { type: 'errorMark', diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts index f1f0163a49d105..e2b00c13c5c1f4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Marks/get_error_marks.ts @@ -6,7 +6,6 @@ import { isEmpty } from 'lodash'; import { ErrorRaw } from '../../../../../../../typings/es_schemas/raw/ErrorRaw'; import { - IWaterfallItem, IWaterfallError, IServiceColors } from '../Waterfall/waterfall_helpers/waterfall_helpers'; @@ -19,16 +18,14 @@ export interface ErrorMark extends Mark { } export const getErrorMarks = ( - items: IWaterfallItem[], + errorItems: IWaterfallError[], serviceColors: IServiceColors ): ErrorMark[] => { - if (isEmpty(items)) { + if (isEmpty(errorItems)) { return []; } - return (items.filter( - item => item.docType === 'error' - ) as IWaterfallError[]).map(error => ({ + return errorItems.map(error => ({ type: 'errorMark', offset: error.offset + error.skew, verticalLine: false, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx index b48fc1cf7ca27e..4f584786f2f9a5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/index.tsx @@ -80,13 +80,9 @@ export const Waterfall: React.FC = ({ const { serviceColors, duration } = waterfall; const agentMarks = getAgentMarks(waterfall.entryTransaction); - const errorMarks = getErrorMarks(waterfall.items, serviceColors); + const errorMarks = getErrorMarks(waterfall.errorItems, serviceColors); const renderWaterfallItem = (item: IWaterfallItem) => { - if (item.docType === 'error') { - return null; - } - const errorCount = item.docType === 'transaction' ? waterfall.errorsPerTransaction[item.doc.transaction.id] diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap index 5b1b9be33c3753..c9b29e8692f447 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/__snapshots__/waterfall_helpers.test.ts.snap @@ -24,6 +24,77 @@ Object { "name": "GET /api", }, }, + "errorItems": Array [ + Object { + "doc": Object { + "agent": Object { + "name": "ruby", + "version": "2", + }, + "error": Object { + "grouping_key": "errorGroupingKey1", + "id": "error1", + "log": Object { + "message": "error message", + }, + }, + "parent": Object { + "id": "myTransactionId1", + }, + "processor": Object { + "event": "error", + }, + "service": Object { + "name": "opbeans-ruby", + }, + "timestamp": Object { + "us": 1549324795810000, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "id": "myTransactionId1", + }, + }, + "docType": "error", + "duration": 0, + "id": "error1", + "offset": 25994, + "parent": Object { + "doc": Object { + "processor": Object { + "event": "transaction", + }, + "service": Object { + "name": "opbeans-node", + }, + "timestamp": Object { + "us": 1549324795784006, + }, + "trace": Object { + "id": "myTraceId", + }, + "transaction": Object { + "duration": Object { + "us": 49660, + }, + "id": "myTransactionId1", + "name": "GET /api", + }, + }, + "docType": "transaction", + "duration": 49660, + "id": "myTransactionId1", + "offset": 0, + "parent": undefined, + "parentId": undefined, + "skew": 0, + }, + "parentId": "myTransactionId1", + "skew": 0, + }, + ], "errorsCount": 1, "errorsPerTransaction": Object { "myTransactionId1": 2, @@ -716,75 +787,6 @@ Object { "parentId": "mySpanIdA", "skew": 0, }, - Object { - "doc": Object { - "agent": Object { - "name": "ruby", - "version": "2", - }, - "error": Object { - "grouping_key": "errorGroupingKey1", - "id": "error1", - "log": Object { - "message": "error message", - }, - }, - "parent": Object { - "id": "myTransactionId1", - }, - "processor": Object { - "event": "error", - }, - "service": Object { - "name": "opbeans-ruby", - }, - "timestamp": Object { - "us": 1549324795810000, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "id": "myTransactionId1", - }, - }, - "docType": "error", - "duration": 0, - "id": "error1", - "offset": 25994, - "parent": Object { - "doc": Object { - "processor": Object { - "event": "transaction", - }, - "service": Object { - "name": "opbeans-node", - }, - "timestamp": Object { - "us": 1549324795784006, - }, - "trace": Object { - "id": "myTraceId", - }, - "transaction": Object { - "duration": Object { - "us": 49660, - }, - "id": "myTransactionId1", - "name": "GET /api", - }, - }, - "docType": "transaction", - "duration": 49660, - "id": "myTransactionId1", - "offset": 0, - "parent": undefined, - "parentId": undefined, - "skew": 0, - }, - "parentId": "myTransactionId1", - "skew": 0, - }, ], "rootTransaction": Object { "processor": Object { @@ -848,6 +850,7 @@ Object { "name": "Api::ProductsController#index", }, }, + "errorItems": Array [], "errorsCount": 0, "errorsPerTransaction": Object { "myTransactionId1": 2, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index 426842bc02f510..6b13b93200c617 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -12,7 +12,8 @@ import { getOrderedWaterfallItems, getWaterfall, IWaterfallItem, - IWaterfallTransaction + IWaterfallTransaction, + IWaterfallError } from './waterfall_helpers'; import { APMError } from '../../../../../../../../typings/es_schemas/ui/APMError'; @@ -100,7 +101,9 @@ describe('waterfall_helpers', () => { } }, timestamp: { us: 1549324795823304 } - } as unknown) as Transaction, + } as unknown) as Transaction + ]; + const errorDocs = [ ({ processor: { event: 'error' }, parent: { id: 'myTransactionId1' }, @@ -130,14 +133,15 @@ describe('waterfall_helpers', () => { }; const waterfall = getWaterfall( { - trace: { items: hits, exceedsMax: false }, + trace: { items: hits, errorDocs, exceedsMax: false }, errorsPerTransaction }, entryTransactionId ); - expect(waterfall.items.length).toBe(7); + expect(waterfall.items.length).toBe(6); expect(waterfall.items[0].id).toBe('myTransactionId1'); + expect(waterfall.errorItems.length).toBe(1); expect(waterfall.errorsCount).toEqual(1); expect(waterfall).toMatchSnapshot(); }); @@ -150,7 +154,7 @@ describe('waterfall_helpers', () => { }; const waterfall = getWaterfall( { - trace: { items: hits, exceedsMax: false }, + trace: { items: hits, errorDocs, exceedsMax: false }, errorsPerTransaction }, entryTransactionId @@ -158,6 +162,7 @@ describe('waterfall_helpers', () => { expect(waterfall.items.length).toBe(4); expect(waterfall.items[0].id).toBe('myTransactionId2'); + expect(waterfall.errorItems.length).toBe(0); expect(waterfall.errorsCount).toEqual(0); expect(waterfall).toMatchSnapshot(); }); @@ -386,7 +391,7 @@ describe('waterfall_helpers', () => { it('should return parent skew for errors', () => { const child = { docType: 'error' - } as IWaterfallItem; + } as IWaterfallError; const parent = { docType: 'transaction', diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 1af6cddb3ba4a7..3b52163aa7fa40 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -24,6 +24,8 @@ interface IWaterfallGroup { [key: string]: IWaterfallItem[]; } +const ROOT_ID = 'root'; + export interface IWaterfall { entryTransaction?: Transaction; rootTransaction?: Transaction; @@ -36,6 +38,7 @@ export interface IWaterfall { errorsPerTransaction: TraceAPIResponse['errorsPerTransaction']; errorsCount: number; serviceColors: IServiceColors; + errorItems: IWaterfallError[]; } interface IWaterfallItemBase { @@ -70,10 +73,7 @@ export type IWaterfallTransaction = IWaterfallItemBase< export type IWaterfallSpan = IWaterfallItemBase; export type IWaterfallError = IWaterfallItemBase; -export type IWaterfallItem = - | IWaterfallTransaction - | IWaterfallSpan - | IWaterfallError; +export type IWaterfallItem = IWaterfallTransaction | IWaterfallSpan; function getTransactionItem(transaction: Transaction): IWaterfallTransaction { return { @@ -99,20 +99,34 @@ function getSpanItem(span: Span): IWaterfallSpan { }; } -function getErrorItem(error: APMError): IWaterfallError { - return { +function getErrorItem( + error: APMError, + items: IWaterfallItem[], + entryWaterfallTransaction?: IWaterfallTransaction +): IWaterfallError { + const entryTimestamp = entryWaterfallTransaction?.doc.timestamp.us ?? 0; + const parent = items.find( + waterfallItem => waterfallItem.id === error.parent?.id + ); + const errorItem: IWaterfallError = { docType: 'error', doc: error, id: error.error.id, - parentId: error.parent?.id, - offset: 0, + parent, + parentId: parent?.id, + offset: error.timestamp.us - entryTimestamp, skew: 0, duration: 0 }; + + return { + ...errorItem, + skew: getClockSkew(errorItem, parent) + }; } export function getClockSkew( - item: IWaterfallItem, + item: IWaterfallItem | IWaterfallError, parentItem?: IWaterfallItem ) { if (!parentItem) { @@ -218,13 +232,11 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => return getSpanItem(item as Span); case 'transaction': return getTransactionItem(item as Transaction); - case 'error': - return getErrorItem(item as APMError); } }); const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) => - groupBy(waterfallItems, item => (item.parentId ? item.parentId : 'root')); + groupBy(waterfallItems, item => (item.parentId ? item.parentId : ROOT_ID)); const getEntryWaterfallTransaction = ( entryTransactionId: string, @@ -234,6 +246,48 @@ const getEntryWaterfallTransaction = ( item => item.docType === 'transaction' && item.id === entryTransactionId ) as IWaterfallTransaction; +function isInEntryTransaction( + parentIdLookup: Map, + entryTransactionId: string, + currentId: string +): boolean { + if (currentId === entryTransactionId) { + return true; + } + const parentId = parentIdLookup.get(currentId); + if (parentId) { + return isInEntryTransaction(parentIdLookup, entryTransactionId, parentId); + } + return false; +} + +function getWaterfallErrors( + errorDocs: TraceAPIResponse['trace']['errorDocs'], + items: IWaterfallItem[], + entryWaterfallTransaction?: IWaterfallTransaction +) { + const errorItems = errorDocs.map(errorDoc => + getErrorItem(errorDoc, items, entryWaterfallTransaction) + ); + if (!entryWaterfallTransaction) { + return errorItems; + } + const parentIdLookup = [...items, ...errorItems].reduce( + (map, { id, parentId }) => { + map.set(id, parentId ?? ROOT_ID); + return map; + }, + new Map() + ); + return errorItems.filter(errorItem => + isInEntryTransaction( + parentIdLookup, + entryWaterfallTransaction?.id, + errorItem.id + ) + ); +} + export function getWaterfall( { trace, errorsPerTransaction }: TraceAPIResponse, entryTransactionId?: Transaction['transaction']['id'] @@ -244,7 +298,8 @@ export function getWaterfall( items: [], errorsPerTransaction, errorsCount: sum(Object.values(errorsPerTransaction)), - serviceColors: {} + serviceColors: {}, + errorItems: [] }; } @@ -261,6 +316,11 @@ export function getWaterfall( childrenByParentId, entryWaterfallTransaction ); + const errorItems = getWaterfallErrors( + trace.errorDocs, + items, + entryWaterfallTransaction + ); const rootTransaction = getRootTransaction(childrenByParentId); const duration = getWaterfallDuration(items); @@ -274,7 +334,8 @@ export function getWaterfall( duration, items, errorsPerTransaction, - errorsCount: items.filter(item => item.docType === 'error').length, - serviceColors + errorsCount: errorItems.length, + serviceColors, + errorItems }; } diff --git a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts index f3f10c0e46d9b9..8d623a02682225 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useWaterfall.ts @@ -11,7 +11,7 @@ import { getWaterfall } from '../components/app/TransactionDetails/WaterfallWith const INITIAL_DATA = { root: undefined, - trace: { items: [], exceedsMax: false }, + trace: { items: [], exceedsMax: false, errorDocs: [] }, errorsPerTransaction: {} }; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap index 88d8edd17454a5..a2629366dd6d93 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/errors/__snapshots__/queries.test.ts.snap @@ -205,67 +205,3 @@ Object { "index": "myIndex", } `; - -exports[`error queries fetches trace errors 1`] = ` -Object { - "body": Object { - "aggs": Object { - "transactions": Object { - "terms": Object { - "execution_hint": "map", - "field": "transaction.id", - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "trace.id": "foo", - }, - }, - Object { - "term": Object { - "processor.event": "error", - }, - }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, - ], - "should": Array [ - Object { - "bool": Object { - "must_not": Array [ - Object { - "exists": Object { - "field": "error.log.level", - }, - }, - ], - }, - }, - Object { - "terms": Object { - "error.log.level": Array [ - "critical", - "error", - "fatal", - ], - }, - }, - ], - }, - }, - "size": 0, - }, - "index": "myIndex", -} -`; diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts b/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts deleted file mode 100644 index 6027693be51801..00000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/errors/get_trace_errors_per_transaction.ts +++ /dev/null @@ -1,65 +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 { - ERROR_LOG_LEVEL, - PROCESSOR_EVENT, - TRACE_ID, - TRANSACTION_ID -} from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../helpers/range_filter'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; - -export interface ErrorsPerTransaction { - [transactionId: string]: number; -} - -const includedLogLevels = ['critical', 'error', 'fatal']; - -export async function getTraceErrorsPerTransaction( - traceId: string, - setup: Setup & SetupTimeRange -): Promise { - const { start, end, client, indices } = setup; - - const params = { - index: indices['apm_oss.errorIndices'], - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [TRACE_ID]: traceId } }, - { term: { [PROCESSOR_EVENT]: 'error' } }, - { range: rangeFilter(start, end) } - ], - should: [ - { bool: { must_not: [{ exists: { field: ERROR_LOG_LEVEL } }] } }, - { terms: { [ERROR_LOG_LEVEL]: includedLogLevels } } - ] - } - }, - aggs: { - transactions: { - terms: { - field: TRANSACTION_ID, - // high cardinality - execution_hint: 'map' - } - } - } - } - } as const; - - const resp = await client.search(params); - return (resp.aggregations?.transactions.buckets || []).reduce( - (acc, bucket) => ({ - ...acc, - [bucket.key]: bucket.doc_count - }), - {} - ); -} diff --git a/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts index 2b1704d9424e47..f1e5d31efd4bdb 100644 --- a/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/errors/queries.test.ts @@ -6,7 +6,6 @@ import { getErrorGroup } from './get_error_group'; import { getErrorGroups } from './get_error_groups'; -import { getTraceErrorsPerTransaction } from './get_trace_errors_per_transaction'; import { SearchParamsMock, inspectSearchParams @@ -56,12 +55,4 @@ describe('error queries', () => { expect(mock.params).toMatchSnapshot(); }); - - it('fetches trace errors', async () => { - mock = await inspectSearchParams(setup => - getTraceErrorsPerTransaction('foo', setup) - ); - - expect(mock.params).toMatchSnapshot(); - }); }); diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap index a2828e1d74920a..0a9f9d38b2be70 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/traces/__snapshots__/queries.test.ts.snap @@ -3,6 +3,15 @@ exports[`trace queries fetches a trace 1`] = ` Object { "body": Object { + "aggs": Object { + "by_transaction_id": Object { + "terms": Object { + "execution_hint": "map", + "field": "transaction.id", + "size": "myIndex", + }, + }, + }, "query": Object { "bool": Object { "filter": Array [ @@ -12,12 +21,8 @@ Object { }, }, Object { - "terms": Object { - "processor.event": Array [ - "span", - "transaction", - "error", - ], + "term": Object { + "processor.event": "error", }, }, Object { @@ -30,36 +35,19 @@ Object { }, }, ], - "should": Object { - "exists": Object { - "field": "parent.id", + "must_not": Object { + "terms": Object { + "error.log.level": Array [ + "debug", + "info", + "warning", + ], }, }, }, }, "size": "myIndex", - "sort": Array [ - Object { - "_score": Object { - "order": "asc", - }, - }, - Object { - "transaction.duration.us": Object { - "order": "desc", - }, - }, - Object { - "span.duration.us": Object { - "order": "desc", - }, - }, - ], - "track_total_hits": true, }, - "index": Array [ - "myIndex", - "myIndex", - ], + "index": "myIndex", } `; diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts index e38ce56edde80e..a1b9270e0d7b3b 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts +++ b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace.ts @@ -5,16 +5,15 @@ */ import { PromiseReturnType } from '../../../typings/common'; -import { getTraceErrorsPerTransaction } from '../errors/get_trace_errors_per_transaction'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTraceItems } from './get_trace_items'; export type TraceAPIResponse = PromiseReturnType; export async function getTrace(traceId: string, setup: Setup & SetupTimeRange) { - const [trace, errorsPerTransaction] = await Promise.all([ - getTraceItems(traceId, setup), - getTraceErrorsPerTransaction(traceId, setup) - ]); + const { errorsPerTransaction, ...trace } = await getTraceItems( + traceId, + setup + ); return { trace, diff --git a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts index 8118b6acaee397..9d3e0d6db7f16f 100644 --- a/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/legacy/plugins/apm/server/lib/traces/get_trace_items.ts @@ -9,13 +9,20 @@ import { TRACE_ID, PARENT_ID, TRANSACTION_DURATION, - SPAN_DURATION + SPAN_DURATION, + TRANSACTION_ID, + ERROR_LOG_LEVEL } from '../../../common/elasticsearch_fieldnames'; import { Span } from '../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../typings/es_schemas/ui/Transaction'; import { APMError } from '../../../typings/es_schemas/ui/APMError'; import { rangeFilter } from '../helpers/range_filter'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { PromiseValueType } from '../../../typings/common'; + +interface ErrorsPerTransaction { + [transactionId: string]: number; +} export async function getTraceItems( traceId: string, @@ -23,8 +30,36 @@ export async function getTraceItems( ) { const { start, end, client, config, indices } = setup; const maxTraceItems = config['xpack.apm.ui.maxTraceItems']; + const excludedLogLevels = ['debug', 'info', 'warning']; - const params = { + const errorResponsePromise = client.search({ + index: indices['apm_oss.errorIndices'], + body: { + size: maxTraceItems, + query: { + bool: { + filter: [ + { term: { [TRACE_ID]: traceId } }, + { term: { [PROCESSOR_EVENT]: 'error' } }, + { range: rangeFilter(start, end) } + ], + must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } } + } + }, + aggs: { + by_transaction_id: { + terms: { + field: TRANSACTION_ID, + size: maxTraceItems, + // high cardinality + execution_hint: 'map' as const + } + } + } + } + }); + + const traceResponsePromise = client.search({ index: [ indices['apm_oss.spanIndices'], indices['apm_oss.transactionIndices'] @@ -35,7 +70,7 @@ export async function getTraceItems( bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { terms: { [PROCESSOR_EVENT]: ['span', 'transaction', 'error'] } }, + { terms: { [PROCESSOR_EVENT]: ['span', 'transaction'] } }, { range: rangeFilter(start, end) } ], should: { @@ -50,12 +85,43 @@ export async function getTraceItems( ], track_total_hits: true } - }; + }); + + const [errorResponse, traceResponse]: [ + // explicit intermediary types to avoid TS "excessively deep" error + PromiseValueType, + PromiseValueType + // @ts-ignore + ] = await Promise.all([errorResponsePromise, traceResponsePromise]); + + const exceedsMax = traceResponse.hits.total.value > maxTraceItems; - const resp = await client.search(params); + const items = (traceResponse.hits.hits as Array<{ + _source: Transaction | Span; + }>).map(hit => hit._source); + + const errorFrequencies: { + errorsPerTransaction: ErrorsPerTransaction; + errorDocs: APMError[]; + } = { + errorDocs: errorResponse.hits.hits.map( + ({ _source }) => _source as APMError + ), + errorsPerTransaction: + errorResponse.aggregations?.by_transaction_id.buckets.reduce( + (acc, current) => { + return { + ...acc, + [current.key]: current.doc_count + }; + }, + {} as ErrorsPerTransaction + ) ?? {} + }; return { - items: resp.hits.hits.map(hit => hit._source), - exceedsMax: resp.hits.total.value > maxTraceItems + items, + exceedsMax, + ...errorFrequencies }; } diff --git a/x-pack/legacy/plugins/apm/typings/common.d.ts b/x-pack/legacy/plugins/apm/typings/common.d.ts index b9064980bd6577..1e718f818246c9 100644 --- a/x-pack/legacy/plugins/apm/typings/common.d.ts +++ b/x-pack/legacy/plugins/apm/typings/common.d.ts @@ -22,6 +22,10 @@ type AllowUnknownObjectProperties = T extends object } : T; +export type PromiseValueType = Value extends Promise + ? Value + : Value; + export type PromiseReturnType = Func extends ( ...args: any[] ) => Promise From 077d24de10a79f8a747782d54d1c574a876bed32 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 3 Feb 2020 13:14:42 -0800 Subject: [PATCH 53/62] Closes #55502. Fixes firefox SVG error by preventing tooltip and marks (#56578) from rendering if there are no values in the Plot. Co-authored-by: Elastic Machine --- .../components/shared/charts/CustomPlot/InteractivePlot.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js index bc758c7288e96e..69b73ad6a0c0fa 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/CustomPlot/InteractivePlot.js @@ -66,9 +66,13 @@ class InteractivePlot extends PureComponent { const tooltipPoints = this.getTooltipPoints(hoverX); const markPoints = this.getMarkPoints(hoverX); - const { x, yTickValues } = plotValues; + const { x, xTickValues, yTickValues } = plotValues; const yValueMiddle = yTickValues[1]; + if (isEmpty(xTickValues)) { + return ; + } + return ( {hoverX && ( From e08df006c3a3d49ec8612c35298d2a68be12fced Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 3 Feb 2020 13:19:23 -0800 Subject: [PATCH 54/62] [APM] Fix initial error sort field (#56577) * Closes #52840. Changes the initial sorting field from `latestOccurrenceAt` -> `occurrenceCount` * update jest snapshots Co-authored-by: Elastic Machine --- .../__test__/__snapshots__/List.test.tsx.snap | 68 +++++++++---------- .../app/ErrorGroupOverview/List/index.tsx | 2 +- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap index 492d28206f3dda..a45357121354f1 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap @@ -43,7 +43,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` } initialPageSize={25} initialSortDirection="desc" - initialSortField="latestOccurrenceAt" + initialSortField="occurrenceCount" items={Array []} noItemsMessage="No errors were found" sortItems={false} @@ -190,7 +190,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` List should render empty state 1`] = ` } >
"`; +exports[`VisLegend Component Legend closed should match the snapshot 1`] = `"
"`; -exports[`VisLegend Component Legend open should match the snapshot 1`] = `"
"`; +exports[`VisLegend Component Legend open should match the snapshot 1`] = `"
"`; diff --git a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap index 18f84f41d5d998..307c0760de7ba4 100644 --- a/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/kibana_react/public/saved_objects/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -8,7 +8,6 @@ exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index d96febee7b06d2..d320b57ee59e66 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0", "react-dom": "^16.12.0" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 170cc77ca37cc1..27a8c1fab6c8e8 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -7,7 +7,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json index 85c76071d1e944..51bb7240dd7c44 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json index ade93c9f500990..9ee0e3de51d8b5 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_tp_sample_panel_action/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@elastic/eui": "18.2.1", + "@elastic/eui": "18.3.0", "react": "^16.12.0" }, "scripts": { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap index b2c503806c3854..260d7de3aefd4b 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/__snapshots__/index.test.tsx.snap @@ -62,11 +62,7 @@ exports[`DetailView should render TabContent 1`] = ` `; exports[`DetailView should render tabs 1`] = ` - + - <path d="M13.6 12.186l-1.357-1.358c-.025-.025-.058-.034-.084-.056.53-.794.84-1.746.84-2.773a4.977 4.977 0 00-.84-2.772c.026-.02.059-.03.084-.056L13.6 3.813a6.96 6.96 0 010 8.373zM8 15A6.956 6.956 0 013.814 13.6l1.358-1.358c.025-.025.034-.057.055-.084C6.02 12.688 6.974 13 8 13a4.978 4.978 0 002.773-.84c.02.026.03.058.056.083l1.357 1.358A6.956 6.956 0 018 15zm-5.601-2.813a6.963 6.963 0 010-8.373l1.359 1.358c.024.025.057.035.084.056A4.97 4.97 0 003 8c0 1.027.31 1.98.842 2.773-.027.022-.06.031-.084.056l-1.36 1.358zm5.6-.187A4 4 0 118 4a4 4 0 010 8zM8 1c1.573 0 3.019.525 4.187 1.4l-1.357 1.358c-.025.025-.035.057-.056.084A4.979 4.979 0 008 3a4.979 4.979 0 00-2.773.842c-.021-.027-.03-.059-.055-.084L3.814 2.4A6.957 6.957 0 018 1zm0-1a8.001 8.001 0 10.003 16.002A8.001 8.001 0 008 0z" fill-rule="evenodd" diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap index 48e442ce734cf9..5f5f3a2d40f954 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/__snapshots__/TransactionActionMenu.test.tsx.snap @@ -26,7 +26,6 @@ exports[`TransactionActionMenu component should match the snapshot 1`] = ` width="16" xmlns="http://www.w3.org/2000/svg" > - <title /> <path d="M13.069 5.157L8.384 9.768a.546.546 0 01-.768 0L2.93 5.158a.552.552 0 00-.771 0 .53.53 0 000 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 000-.76.552.552 0 00-.771 0z" fill-rule="non-zero" diff --git a/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.stories.storyshot index c1cb45123f04bc..b394fc30c8d60f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/font_picker/__snapshots__/font_picker.stories.storyshot @@ -26,7 +26,8 @@ exports[`Storyshots components/FontPicker default 1`] = ` className="euiScreenReaderOnly" id="generated-id" > - Select an option: , is selected + Select an option: + , is selected </span> <button aria-haspopup="true" @@ -37,9 +38,7 @@ exports[`Storyshots components/FontPicker default 1`] = ` onKeyDown={[Function]} role="option" type="button" - > - - </button> + /> <div className="euiFormControlLayoutIcons euiFormControlLayoutIcons--right" > diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap index acd68622f1af0f..e5e13671057bd6 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/__tests__/__snapshots__/shareable.test.tsx.snap @@ -9,7 +9,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with default propertie </style><div class=\\"content\\"><div class=\\"renderContainer\\"><div data-renderer=\\"markdown\\" class=\\"render\\"><div>markdown mock</div></div></div></div></div></div></div></div></div></div><div class=\\"root\\" style=\\"height: 48px;\\"><div class=\\"root\\"><div class=\\"slideContainer\\"><div class=\\"root\\" style=\\"height: 100px; width: 150px;\\"><div class=\\"preview\\" style=\\"height: 720px; width: 1080px;\\"><div id=\\"page-7186b301-f8a7-4c65-8b89-38d68d31cfc4\\" class=\\"root\\" style=\\"height: 720px; width: 1080px; background: rgb(119, 119, 119);\\"><div class=\\"canvasPositionable canvasInteractable\\" style=\\"width: 1082px; height: 205.37748344370857px; margin-left: -541px; margin-top: -102.68874172185429px; position: absolute;\\"><div class=\\"root\\"><div class=\\"container s2042575598\\" style=\\"overflow: hidden;\\"><style type=\\"text/css\\">.s2042575598 .canvasRenderEl h1 { font-size: 150px; text-align: center; color: #d3d3d3; } -</style><div class=\\"content\\"><div class=\\"renderContainer\\"><div data-renderer=\\"markdown\\" class=\\"render\\"><div>markdown mock</div></div></div></div></div></div></div></div></div></div></div></div><div class=\\"bar\\" style=\\"bottom: 0px;\\"><div class=\\"euiFlexGroup euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem title\\"><div class=\\"euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--alignItemsCenter euiFlexGroup--directionRow euiFlexGroup--responsive\\"><div class=\\"euiFlexItem euiFlexItem--flexGrowZero\\"><a class=\\"euiLink euiLink--primary\\" href=\\"https://www.elastic.co\\" rel=\\"\\" title=\\"Powered by Elastic.co\\"><svg width=\\"32\\" height=\\"32\\" viewBox=\\"0 0 32 32\\" xmlns=\\"http://www.w3.org/2000/svg\\" class=\\"euiIcon euiIcon--large euiIcon-isLoaded\\" focusable=\\"false\\" role=\\"img\\" aria-hidden=\\"true\\"><title>
My Canvas Workpad