diff --git a/.ci/Jenkinsfile_security_cypress b/.ci/Jenkinsfile_security_cypress new file mode 100644 index 0000000000000..bdfef18024b78 --- /dev/null +++ b/.ci/Jenkinsfile_security_cypress @@ -0,0 +1,21 @@ +#!/bin/groovy + +library 'kibana-pipeline-library' +kibanaLibrary.load() + +kibanaPipeline(timeoutMinutes: 180) { + slackNotifications.onFailure( + disabled: !params.NOTIFY_ON_FAILURE, + channel: '#security-solution-slack-testing' + ) { + catchError { + workers.base(size: 's', ramDisk: false) { + kibanaPipeline.bash('test/scripts/jenkins_security_solution_cypress.sh', 'Execute Security Solution Cypress Tests') + } + } + } + + if (params.NOTIFY_ON_FAILURE) { + kibanaPipeline.sendMail(to: 'gloria.delatorre@elastic.co') + } +} diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 3bc710e44f7bc..345156b2491a1 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -136,11 +136,6 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { if (opts.verbose) set('logging.verbose', true); if (opts.logFile) set('logging.dest', opts.logFile); - if (opts.optimize) { - set('server.autoListen', false); - set('plugins.initialize', false); - } - set('plugins.scanDirs', _.compact([].concat(get('plugins.scanDirs'), opts.pluginDir))); set( 'plugins.paths', diff --git a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana index 449fc4e75fce8..c13676ef031b0 100755 --- a/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana +++ b/src/dev/build/tasks/os_packages/service_templates/sysv/etc/init.d/kibana @@ -15,6 +15,24 @@ # Description: Kibana ### END INIT INFO +# +# Source function libraries if present. +# (It improves integration with systemd) +# +# Red Hat +if [ -f /etc/rc.d/init.d/functions ]; then + . /etc/rc.d/init.d/functions + +# Debian +elif [ -f /lib/lsb/init-functions ]; then + . /lib/lsb/init-functions + +# SUSE +elif [ -f /etc/rc.status ]; then + . /etc/rc.status + rc_reset +fi + name=kibana program=/usr/share/kibana/bin/kibana pidfile="/var/run/kibana/$name.pid" diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 74e1ec5e2b4ed..d46b955f6668d 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -78,6 +78,7 @@ export default { setupFilesAfterEnv: [ '/src/dev/jest/setup/mocks.js', '/src/dev/jest/setup/react_testing_library.js', + '/src/dev/jest/setup/default_timeout.js', ], coverageDirectory: '/target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], diff --git a/src/dev/jest/setup/default_timeout.js b/src/dev/jest/setup/default_timeout.js new file mode 100644 index 0000000000000..eea38e745b960 --- /dev/null +++ b/src/dev/jest/setup/default_timeout.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +/* eslint-env jest */ + +/** + * Set the default timeout for the unit tests to 30 seconds, temporarily + */ +jest.setTimeout(30 * 1000); diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 007be9da63e49..00895ec49003b 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -59,6 +59,7 @@ &.kbnQueryBar__datePickerWrapper-isHidden { width: 0; overflow: hidden; + max-width: 0; } } } diff --git a/src/plugins/discover/public/application/angular/doc.html b/src/plugins/discover/public/application/angular/doc.html index fc1bff7eef6ec..dcd5760eff155 100644 --- a/src/plugins/discover/public/application/angular/doc.html +++ b/src/plugins/discover/public/application/angular/doc.html @@ -1,6 +1,5 @@
{ .when('/doc/:indexPattern/:index', { // have to be written as function expression, because it's not compiled in dev mode // eslint-disable-next-line object-shorthand - controller: function ($scope: LazyScope, $route: any, es: any) { + controller: function ($scope: LazyScope, $route: any) { timefilter.disableAutoRefreshSelector(); timefilter.disableTimeRangeSelector(); - $scope.esClient = es; $scope.id = $route.current.params.id; $scope.index = $route.current.params.index; $scope.indexPatternId = $route.current.params.indexPattern; diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx index c9fa551f61aca..d562291db46ac 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { throwError, of } from 'rxjs'; import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; @@ -23,6 +24,8 @@ import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import { Doc, DocProps } from './doc'; +const mockSearchApi = jest.fn(); + jest.mock('../../../kibana_services', () => { let registry: any[] = []; @@ -31,6 +34,11 @@ jest.mock('../../../kibana_services', () => { metadata: { branch: 'test', }, + data: { + search: { + search: mockSearchApi, + }, + }, }), getDocViewsRegistry: () => ({ addDocView(view: any) { @@ -59,7 +67,7 @@ const waitForPromises = async () => * this works but logs ugly error messages until we're using React 16.9 * should be adapted when we upgrade */ -async function mountDoc(search: () => void, update = false, indexPatternGetter: any = null) { +async function mountDoc(update = false, indexPatternGetter: any = null) { const indexPattern = { getComputedFields: () => [], }; @@ -70,7 +78,6 @@ async function mountDoc(search: () => void, update = false, indexPatternGetter: const props = { id: '1', index: 'index1', - esClient: { search } as any, indexPatternId: 'xyz', indexPatternService, } as DocProps; @@ -88,32 +95,33 @@ async function mountDoc(search: () => void, update = false, indexPatternGetter: describe('Test of of Discover', () => { test('renders loading msg', async () => { - const comp = await mountDoc(jest.fn()); + const comp = await mountDoc(); expect(findTestSubject(comp, 'doc-msg-loading').length).toBe(1); }); test('renders IndexPattern notFound msg', async () => { const indexPatternGetter = jest.fn(() => Promise.reject({ savedObjectId: '007' })); - const comp = await mountDoc(jest.fn(), true, indexPatternGetter); + const comp = await mountDoc(true, indexPatternGetter); expect(findTestSubject(comp, 'doc-msg-notFoundIndexPattern').length).toBe(1); }); test('renders notFound msg', async () => { - const search = jest.fn(() => Promise.reject({ status: 404 })); - const comp = await mountDoc(search, true); + mockSearchApi.mockImplementation(() => throwError({ status: 404 })); + const comp = await mountDoc(true); expect(findTestSubject(comp, 'doc-msg-notFound').length).toBe(1); }); test('renders error msg', async () => { - const search = jest.fn(() => Promise.reject('whatever')); - const comp = await mountDoc(search, true); + mockSearchApi.mockImplementation(() => throwError({ error: 'something else' })); + const comp = await mountDoc(true); expect(findTestSubject(comp, 'doc-msg-error').length).toBe(1); }); test('renders elasticsearch hit ', async () => { - const hit = { hits: { total: 1, hits: [{ _id: 1, _source: { test: 1 } }] } }; - const search = jest.fn(() => Promise.resolve(hit)); - const comp = await mountDoc(search, true); + mockSearchApi.mockImplementation(() => + of({ rawResponse: { hits: { total: 1, hits: [{ _id: 1, _source: { test: 1 } }] } } }) + ); + const comp = await mountDoc(true); expect(findTestSubject(comp, 'doc-hit').length).toBe(1); }); }); diff --git a/src/plugins/discover/public/application/components/doc/doc.tsx b/src/plugins/discover/public/application/components/doc/doc.tsx index 0e31ded267b75..2623b5a270a31 100644 --- a/src/plugins/discover/public/application/components/doc/doc.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.tsx @@ -23,17 +23,6 @@ import { IndexPatternsContract } from 'src/plugins/data/public'; import { ElasticRequestState, useEsDocSearch } from './use_es_doc_search'; import { getServices } from '../../../kibana_services'; import { DocViewer } from '../doc_viewer/doc_viewer'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; - -export interface ElasticSearchResult { - hits: { - hits: [ElasticSearchHit]; - max_score: number; - }; - timed_out: boolean; - took: number; - shards: Record; -} export interface DocProps { /** @@ -53,12 +42,6 @@ export interface DocProps { * IndexPatternService to get a given index pattern by ID */ indexPatternService: IndexPatternsContract; - /** - * Client of ElasticSearch to use for the query - */ - esClient: { - search: (payload: { index: string; body: Record }) => Promise; - }; } export function Doc(props: DocProps) { diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx index a8fe8de833315..e0d505f9aa6fa 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx @@ -19,6 +19,21 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_doc_search'; import { DocProps } from './doc'; +import { Observable } from 'rxjs'; + +const mockSearchResult = new Observable(); + +jest.mock('../../../kibana_services', () => ({ + getServices: () => ({ + data: { + search: { + search: jest.fn(() => { + return mockSearchResult; + }), + }, + }, + }), +})); describe('Test of helper / hook', () => { test('buildSearchBody', () => { @@ -53,7 +68,6 @@ describe('Test of helper / hook', () => { const props = { id: '1', index: 'index1', - esClient: { search: jest.fn(() => new Promise(() => {})) }, indexPatternId: 'xyz', indexPatternService, } as DocProps; diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts index 00496a3a72681..522ebad1691a9 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts @@ -17,7 +17,7 @@ * under the License. */ import { useEffect, useState } from 'react'; -import { IndexPattern } from '../../../kibana_services'; +import { IndexPattern, getServices } from '../../../kibana_services'; import { DocProps } from './doc'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; @@ -53,7 +53,6 @@ export function buildSearchBody(id: string, indexPattern: IndexPattern): Record< * Custom react hook for querying a single doc in ElasticSearch */ export function useEsDocSearch({ - esClient, id, index, indexPatternId, @@ -69,12 +68,18 @@ export function useEsDocSearch({ const indexPatternEntity = await indexPatternService.get(indexPatternId); setIndexPattern(indexPatternEntity); - const { hits } = await esClient.search({ - index, - body: buildSearchBody(id, indexPatternEntity), - }); + const { rawResponse } = await getServices() + .data.search.search({ + params: { + index, + body: buildSearchBody(id, indexPatternEntity), + }, + }) + .toPromise(); - if (hits && hits.hits && hits.hits[0]) { + const hits = rawResponse.hits; + + if (hits?.hits?.[0]) { setStatus(ElasticRequestState.Found); setHit(hits.hits[0]); } else { @@ -91,6 +96,6 @@ export function useEsDocSearch({ } } requestData(); - }, [esClient, id, index, indexPatternId, indexPatternService]); + }, [id, index, indexPatternId, indexPatternService]); return [status, hit, indexPattern]; } diff --git a/src/plugins/discover/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap b/src/plugins/discover/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap index ee88ce6088d7e..d6f48a9b3c774 100644 --- a/src/plugins/discover/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap +++ b/src/plugins/discover/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap @@ -9,6 +9,9 @@ exports[`returns the \`JsonCodeEditor\` component 1`] = ` > { "_index": "test", + "_type": "doc", + "_id": "foo", + "_score": 1, "_source": { "test": 123 } diff --git a/src/plugins/discover/public/application/components/json_code_block/json_code_block.test.tsx b/src/plugins/discover/public/application/components/json_code_block/json_code_block.test.tsx index 3cbcab5036251..a737b3954ceea 100644 --- a/src/plugins/discover/public/application/components/json_code_block/json_code_block.test.tsx +++ b/src/plugins/discover/public/application/components/json_code_block/json_code_block.test.tsx @@ -23,7 +23,7 @@ import { IndexPattern } from '../../../../../data/public'; it('returns the `JsonCodeEditor` component', () => { const props = { - hit: { _index: 'test', _source: { test: 123 } }, + hit: { _index: 'test', _type: 'doc', _id: 'foo', _score: 1, _source: { test: 123 } }, columns: [], indexPattern: {} as IndexPattern, filter: jest.fn(), diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 5b840a25d8beb..07e9e0a129a26 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -74,6 +74,8 @@ describe('DocViewTable at Discover', () => { const hit = { _index: 'logstash-2014.09.09', + _type: 'doc', + _id: 'id123', _score: 1, _source: { message: @@ -191,6 +193,8 @@ describe('DocViewTable at Discover Doc', () => { const hit = { _index: 'logstash-2014.09.09', _score: 1, + _type: 'doc', + _id: 'id123', _source: { extension: 'html', not_mapped: 'yes', @@ -213,6 +217,9 @@ describe('DocViewTable at Discover Context', () => { // here no toggleColumnButtons are rendered const hit = { _index: 'logstash-2014.09.09', + _type: 'doc', + _id: 'id123', + _score: 1, _source: { message: 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. \ diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 0c86c4f812749..6c90861e26727 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -18,6 +18,7 @@ */ import { ComponentType } from 'react'; import { IScope } from 'angular'; +import { SearchResponse } from 'elasticsearch'; import { IndexPattern } from '../../../../data/public'; export interface AngularDirective { @@ -27,7 +28,7 @@ export interface AngularDirective { export type AngularScope = IScope; -export type ElasticSearchHit = Record>; +export type ElasticSearchHit = SearchResponse['hits']['hits'][number]; export interface FieldMapping { filterable?: boolean; diff --git a/src/plugins/discover/public/get_inner_angular.ts b/src/plugins/discover/public/get_inner_angular.ts index 0b3c2fad8d45b..85b0752f13463 100644 --- a/src/plugins/discover/public/get_inner_angular.ts +++ b/src/plugins/discover/public/get_inner_angular.ts @@ -110,7 +110,6 @@ export function initializeInnerAngularModule( createLocalPromiseModule(); createLocalTopNavModule(navigation); createLocalStorageModule(); - createElasticSearchModule(data); createPagerFactoryModule(); createDocTableModule(); initialized = true; @@ -145,7 +144,6 @@ export function initializeInnerAngularModule( 'discoverPromise', 'discoverTopNav', 'discoverLocalStorageProvider', - 'discoverEs', 'discoverDocTable', 'discoverPagerFactory', ]) @@ -201,16 +199,6 @@ const createLocalStorageService = function (type: string) { }; }; -function createElasticSearchModule(data: DataPublicPluginStart) { - angular - .module('discoverEs', []) - // Elasticsearch client used for requesting data. Connects to the /elasticsearch proxy - // have to be written as function expression, because it's not compiled in dev mode - .service('es', function () { - return data.search.__LEGACY.esClient; - }); -} - function createPagerFactoryModule() { angular.module('discoverPagerFactory', []).factory('pagerFactory', createPagerFactory); } diff --git a/src/plugins/expressions/common/expression_types/specs/boolean.ts b/src/plugins/expressions/common/expression_types/specs/boolean.ts index d730f95d7c423..adbdeafc34fd2 100644 --- a/src/plugins/expressions/common/expression_types/specs/boolean.ts +++ b/src/plugins/expressions/common/expression_types/specs/boolean.ts @@ -41,7 +41,8 @@ export const boolean: ExpressionTypeDefinition<'boolean', boolean> = { }, datatable: (value): Datatable => ({ type: 'datatable', - columns: [{ name: 'value', type: name }], + meta: {}, + columns: [{ id: 'value', name: 'value', meta: { type: name } }], rows: [{ value }], }), }, diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index 5cd53df663e1d..dd3c653878de7 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -23,6 +23,13 @@ import { ExpressionTypeDefinition } from '../types'; import { PointSeries, PointSeriesColumn } from './pointseries'; import { ExpressionValueRender } from './render'; +type State = string | number | boolean | null | undefined | SerializableState; + +/** @internal **/ +export interface SerializableState { + [key: string]: State | State[]; +} + const name = 'datatable'; /** @@ -42,12 +49,23 @@ export type DatatableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'nu */ export type DatatableRow = Record; +export interface DatatableColumnMeta { + type: DatatableColumnType; + field?: string; + params?: SerializableState; +} /** * This type represents the shape of a column in a `Datatable`. */ export interface DatatableColumn { + id: string; name: string; - type: DatatableColumnType; + meta: DatatableColumnMeta; +} + +export interface DatatableMeta { + type?: string; + source?: string; } /** @@ -55,6 +73,7 @@ export interface DatatableColumn { */ export interface Datatable { type: typeof name; + meta?: DatatableMeta; columns: DatatableColumn[]; rows: DatatableRow[]; } @@ -103,14 +122,16 @@ export const datatable: ExpressionTypeDefinition ({ type: name, + meta: {}, rows: [], columns: [], }), pointseries: (value: PointSeries) => ({ type: name, + meta: {}, rows: value.rows, columns: map(value.columns, (val: PointSeriesColumn, colName) => { - return { name: colName, type: val.type }; + return { id: colName, name: colName, meta: { type: val.type } }; }), }), }, @@ -127,13 +148,13 @@ export const datatable: ExpressionTypeDefinition { const validFields = ['x', 'y', 'color', 'size', 'text']; - const columns = table.columns.filter((column) => validFields.includes(column.name)); + const columns = table.columns.filter((column) => validFields.includes(column.id)); const rows = table.rows.map((row) => pick(row, validFields)); return { type: 'pointseries', columns: columns.reduce>((acc, column) => { acc[column.name] = { - type: column.type, + type: column.meta.type, expression: column.name, role: 'dimension', }; diff --git a/src/plugins/expressions/common/expression_types/specs/num.ts b/src/plugins/expressions/common/expression_types/specs/num.ts index 191e617fdc858..041747f39740b 100644 --- a/src/plugins/expressions/common/expression_types/specs/num.ts +++ b/src/plugins/expressions/common/expression_types/specs/num.ts @@ -73,7 +73,8 @@ export const num: ExpressionTypeDefinition<'num', ExpressionValueNum> = { }, datatable: ({ value }): Datatable => ({ type: 'datatable', - columns: [{ name: 'value', type: 'number' }], + meta: {}, + columns: [{ id: 'value', name: 'value', meta: { type: 'number' } }], rows: [{ value }], }), }, diff --git a/src/plugins/expressions/common/expression_types/specs/number.ts b/src/plugins/expressions/common/expression_types/specs/number.ts index 10986659c7848..c5fdacf3408a1 100644 --- a/src/plugins/expressions/common/expression_types/specs/number.ts +++ b/src/plugins/expressions/common/expression_types/specs/number.ts @@ -55,7 +55,8 @@ export const number: ExpressionTypeDefinition = { }, datatable: (value): Datatable => ({ type: 'datatable', - columns: [{ name: 'value', type: 'number' }], + meta: {}, + columns: [{ id: 'value', name: 'value', meta: { type: 'number' } }], rows: [{ value }], }), }, diff --git a/src/plugins/expressions/common/expression_types/specs/string.ts b/src/plugins/expressions/common/expression_types/specs/string.ts index 46f460891c2fb..3d52707279bfc 100644 --- a/src/plugins/expressions/common/expression_types/specs/string.ts +++ b/src/plugins/expressions/common/expression_types/specs/string.ts @@ -40,7 +40,8 @@ export const string: ExpressionTypeDefinition = { }, datatable: (value): Datatable => ({ type: 'datatable', - columns: [{ name: 'value', type: 'string' }], + meta: {}, + columns: [{ id: 'value', name: 'value', meta: { type: 'string' } }], rows: [{ value }], }), }, diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index 6b9c7d1c52db9..14612ab1b2a57 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -48,6 +48,10 @@ export interface MapsLegacyConfigType { includeElasticMapsService: boolean; proxyElasticMapsServiceInMaps: boolean; tilemap: any; + emsFontLibraryUrl: string; + emsFileApiUrl: string; + emsTileApiUrl: string; + emsLandingPageUrl: string; } export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts index df89c9c2f70e9..f65a72f334d07 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts @@ -115,7 +115,7 @@ export class UiActionsExecutionService { context, trigger, })), - title: tasks[0].trigger.title, // title of context menu is title of trigger which originated the chain + title: '', // intentionally don't have any title closeMenu: () => { tasks.forEach((t) => t.defer.resolve()); session.close(); diff --git a/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts index fa9ace1a36c69..aa54706476a8f 100644 --- a/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/apply_filter_trigger.ts @@ -17,11 +17,16 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { Trigger } from '.'; export const APPLY_FILTER_TRIGGER = 'FILTER_TRIGGER'; export const applyFilterTrigger: Trigger<'FILTER_TRIGGER'> = { id: APPLY_FILTER_TRIGGER, - title: 'Apply filter', - description: 'Triggered when user applies filter to an embeddable.', + title: i18n.translate('uiActions.triggers.applyFilterTitle', { + defaultMessage: 'Apply filter', + }), + description: i18n.translate('uiActions.triggers.applyFilterDescription', { + defaultMessage: 'When kibana filter is applied. Could be a single value or a range filter.', + }), }; diff --git a/src/plugins/ui_actions/public/triggers/select_range_trigger.ts b/src/plugins/ui_actions/public/triggers/select_range_trigger.ts index c7c998907381a..f6d5547f62481 100644 --- a/src/plugins/ui_actions/public/triggers/select_range_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/select_range_trigger.ts @@ -17,13 +17,16 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { Trigger } from '.'; export const SELECT_RANGE_TRIGGER = 'SELECT_RANGE_TRIGGER'; export const selectRangeTrigger: Trigger<'SELECT_RANGE_TRIGGER'> = { id: SELECT_RANGE_TRIGGER, - // This is empty string to hide title of ui_actions context menu that appears - // when this trigger is executed. - title: '', - description: 'Applies a range filter', + title: i18n.translate('uiActions.triggers.selectRangeTitle', { + defaultMessage: 'Range selection', + }), + description: i18n.translate('uiActions.triggers.selectRangeDescription', { + defaultMessage: 'Select a group of values', + }), }; diff --git a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts index 5fe060f55dc77..e1e7b6507d82b 100644 --- a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts +++ b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts @@ -17,13 +17,16 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { Trigger } from '.'; export const VALUE_CLICK_TRIGGER = 'VALUE_CLICK_TRIGGER'; export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = { id: VALUE_CLICK_TRIGGER, - // This is empty string to hide title of ui_actions context menu that appears - // when this trigger is executed. - title: '', - description: 'Value was clicked', + title: i18n.translate('uiActions.triggers.valueClickTitle', { + defaultMessage: 'Single click', + }), + description: i18n.translate('uiActions.triggers.valueClickDescription', { + defaultMessage: 'A single point clicked on a visualization', + }), }; diff --git a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap index 001382d946df6..639559dff3091 100644 --- a/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap +++ b/src/plugins/vis_type_vega/public/__snapshots__/vega_visualization.test.js.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`VegaVisualizations VegaVisualization - basics should show vega blank rectangle on top of a map (vegamap) 1`] = `"
"`; +exports[`VegaVisualizations VegaVisualization - basics should show vega blank rectangle on top of a map (vegamap) 1`] = `"
"`; -exports[`VegaVisualizations VegaVisualization - basics should show vega graph (may fail in dev env) 1`] = `"
"`; +exports[`VegaVisualizations VegaVisualization - basics should show vega graph (may fail in dev env) 1`] = `"
"`; -exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 1`] = `"
"`; +exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 1`] = `"
"`; -exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 2`] = `"
"`; +exports[`VegaVisualizations VegaVisualization - basics should show vegalite graph and update on resize (may fail in dev env) 2`] = `"
"`; diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index ac92f31b890ed..4883a8129903a 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -18,6 +18,7 @@ */ import { cloneDeep } from 'lodash'; +import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; import { VegaParser } from './vega_parser'; import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; @@ -70,8 +71,31 @@ describe(`VegaParser._setDefaultColors`, () => { `vegalite`, check({}, true, { config: { + axis: { + domainColor: euiThemeVars.euiColorChartLines, + gridColor: euiThemeVars.euiColorChartLines, + tickColor: euiThemeVars.euiColorChartLines, + }, + background: 'transparent', range: { category: { scheme: 'elastic' } }, mark: { color: '#54B399' }, + style: { + 'group-title': { + fill: euiThemeVars.euiColorDarkestShade, + }, + 'guide-label': { + fill: euiThemeVars.euiColorDarkShade, + }, + 'guide-title': { + fill: euiThemeVars.euiColorDarkestShade, + }, + 'group-subtitle': { + fill: euiThemeVars.euiColorDarkestShade, + }, + }, + title: { + color: euiThemeVars.euiColorDarkestShade, + }, }, }) ); @@ -80,6 +104,12 @@ describe(`VegaParser._setDefaultColors`, () => { `vega`, check({}, false, { config: { + axis: { + domainColor: euiThemeVars.euiColorChartLines, + gridColor: euiThemeVars.euiColorChartLines, + tickColor: euiThemeVars.euiColorChartLines, + }, + background: 'transparent', range: { category: { scheme: 'elastic' } }, arc: { fill: '#54B399' }, area: { fill: '#54B399' }, @@ -90,6 +120,23 @@ describe(`VegaParser._setDefaultColors`, () => { shape: { stroke: '#54B399' }, symbol: { fill: '#54B399' }, trail: { fill: '#54B399' }, + style: { + 'group-title': { + fill: euiThemeVars.euiColorDarkestShade, + }, + 'guide-label': { + fill: euiThemeVars.euiColorDarkShade, + }, + 'guide-title': { + fill: euiThemeVars.euiColorDarkestShade, + }, + 'group-subtitle': { + fill: euiThemeVars.euiColorDarkestShade, + }, + }, + title: { + color: euiThemeVars.euiColorDarkestShade, + }, }, }) ); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index aceeefd953655..da5c76fa6beab 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -21,7 +21,8 @@ import _ from 'lodash'; import schemaParser from 'vega-schema-url-parser'; import versionCompare from 'compare-versions'; import hjson from 'hjson'; -import { VISUALIZATION_COLORS } from '@elastic/eui'; +import { euiPaletteColorBlind } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-shared-deps/theme'; import { i18n } from '@kbn/i18n'; // @ts-ignore import { vega, vegaLite } from '../lib/vega'; @@ -47,7 +48,7 @@ import { } from './types'; // Set default single color to match other Kibana visualizations -const defaultColor: string = VISUALIZATION_COLORS[0]; +const defaultColor: string = euiPaletteColorBlind()[0]; const locToDirMap: Record = { left: 'row-reverse', @@ -659,6 +660,35 @@ The URL is an identifier only. Kibana and your browser will never access this UR this._setDefaultValue(defaultColor, 'config', 'trail', 'fill'); } } + + // provide right colors for light and dark themes + this._setDefaultValue(euiThemeVars.euiColorDarkestShade, 'config', 'title', 'color'); + this._setDefaultValue(euiThemeVars.euiColorDarkShade, 'config', 'style', 'guide-label', 'fill'); + this._setDefaultValue( + euiThemeVars.euiColorDarkestShade, + 'config', + 'style', + 'guide-title', + 'fill' + ); + this._setDefaultValue( + euiThemeVars.euiColorDarkestShade, + 'config', + 'style', + 'group-title', + 'fill' + ); + this._setDefaultValue( + euiThemeVars.euiColorDarkestShade, + 'config', + 'style', + 'group-subtitle', + 'fill' + ); + this._setDefaultValue(euiThemeVars.euiColorChartLines, 'config', 'axis', 'tickColor'); + this._setDefaultValue(euiThemeVars.euiColorChartLines, 'config', 'axis', 'domainColor'); + this._setDefaultValue(euiThemeVars.euiColorChartLines, 'config', 'axis', 'gridColor'); + this._setDefaultValue('transparent', 'config', 'background'); } /** diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index a2a973d232de0..9b51b68e93bb4 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -22,14 +22,14 @@ import moment from 'moment'; import dateMath from '@elastic/datemath'; import { vega, vegaLite } from '../lib/vega'; import { Utils } from '../data_model/utils'; -import { VISUALIZATION_COLORS } from '@elastic/eui'; +import { euiPaletteColorBlind } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TooltipHandler } from './vega_tooltip'; import { esFilters } from '../../../data/public'; import { getEnableExternalUrls } from '../services'; -vega.scheme('elastic', VISUALIZATION_COLORS); +vega.scheme('elastic', euiPaletteColorBlind()); // Vega's extension functions are global. When called, // we forward execution to the instance-specific handler diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 80e577930fa8d..c4d5f5206ee90 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -47,6 +47,7 @@ import { Vis } from '../vis'; import { getExpressions, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; +import { TriggerId } from '../../../ui_actions/public'; const getKeys = (o: T): Array => Object.keys(o) as Array; @@ -402,7 +403,7 @@ export class VisualizeEmbeddable extends Embeddable { + // FLAKY: https://github.com/elastic/kibana/issues/75127 + describe.skip('metric', () => { beforeEach(async () => { await PageObjects.visualBuilder.resetPage(); await PageObjects.visualBuilder.clickMetric(); diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.js b/test/functional/apps/visualize/input_control_vis/chained_controls.js index e1a58e1da34f3..035245b50d436 100644 --- a/test/functional/apps/visualize/input_control_vis/chained_controls.js +++ b/test/functional/apps/visualize/input_control_vis/chained_controls.js @@ -26,7 +26,8 @@ export default function ({ getService, getPageObjects }) { const find = getService('find'); const comboBox = getService('comboBox'); - describe('chained controls', function () { + // FLAKY: https://github.com/elastic/kibana/issues/68472 + describe.skip('chained controls', function () { this.tags('includeFirefox'); before(async () => { diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 173c5b7e11764..00668f2ccdaa7 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -170,7 +170,6 @@ def uploadCoverageArtifacts(prefix, pattern) { def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ - '**/target/public/.kbn-optimizer-cache', 'target/kibana-*', 'target/test-metrics/*', 'target/kibana-security-solution/**/*.png', @@ -221,7 +220,7 @@ def publishJunit() { } } -def sendMail() { +def sendMail(Map params = [:]) { // If the build doesn't have a result set by this point, there haven't been any errors and it can be marked as a success // The e-mail plugin for the infra e-mail depends upon this being set currentBuild.result = currentBuild.result ?: 'SUCCESS' @@ -230,7 +229,7 @@ def sendMail() { if (buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { node('flyweight') { sendInfraMail() - sendKibanaMail() + sendKibanaMail(params) } } } @@ -246,12 +245,14 @@ def sendInfraMail() { } } -def sendKibanaMail() { +def sendKibanaMail(Map params = [:]) { + def config = [to: 'build-kibana@elastic.co'] + params + catchErrors { def buildStatus = buildUtils.getBuildStatus() if(params.NOTIFY_ON_FAILURE && buildStatus != 'SUCCESS' && buildStatus != 'ABORTED') { emailext( - to: 'build-kibana@elastic.co', + config.to, subject: "${env.JOB_NAME} - Build # ${env.BUILD_NUMBER} - ${buildStatus}", body: '${SCRIPT,template="groovy-html.template"}', mimeType: 'text/html', diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index a0574dbdf36da..0e3f82792a2ba 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -58,6 +58,7 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector `${xPackKibanaDirectory}/dev-tools/jest/setup/setup_test.js`, `${kibanaDirectory}/src/dev/jest/setup/mocks.js`, `${kibanaDirectory}/src/dev/jest/setup/react_testing_library.js`, + `${kibanaDirectory}/src/dev/jest/setup/default_timeout.js`, ], testEnvironment: 'jest-environment-jsdom-thirteen', testMatch: ['**/*.test.{js,mjs,ts,tsx}'], diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json index 160352a9afd66..1bae09b488a2e 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json +++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json @@ -5,7 +5,7 @@ "configPath": ["ui_actions_enhanced_examples"], "server": false, "ui": true, - "requiredPlugins": ["uiActionsEnhanced", "data", "discover"], + "requiredPlugins": ["uiActions","uiActionsEnhanced", "data", "discover"], "optionalPlugins": [], "requiredBundles": [ "kibanaUtils", diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx index 2598d66c4976f..fd782f5468c85 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_hello_world_drilldown/index.tsx @@ -10,6 +10,10 @@ import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/publ import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps } from '../../../../../src/plugins/kibana_utils/public'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../src/plugins/ui_actions/public'; export type ActionContext = ChartActionContext; @@ -19,7 +23,8 @@ export interface Config { const SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN = 'SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN'; -export class DashboardHelloWorldDrilldown implements Drilldown { +export class DashboardHelloWorldDrilldown + implements Drilldown { public readonly id = SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN; public readonly order = 6; @@ -28,9 +33,14 @@ export class DashboardHelloWorldDrilldown implements Drilldown { + return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER]; + } + private readonly ReactCollectConfig: React.FC> = ({ config, onConfig, + context, }) => ( { + public readonly id = SAMPLE_DASHBOARD_HELLO_WORLD_DRILLDOWN_ONLY_RANGE_SELECT; + + public readonly order = 7; + + public readonly getDisplayName = () => 'Say hello only for range select'; + + public readonly euiIcon = 'cheer'; + + supportedTriggers(): Array { + return [SELECT_RANGE_TRIGGER]; + } + + private readonly ReactCollectConfig: React.FC> = ({ + config, + onConfig, + }) => ( + + onConfig({ ...config, name: event.target.value })} + /> + + ); + + public readonly CollectConfig = reactToUiComponent(this.ReactCollectConfig); + + public readonly createConfig = () => ({ + name: '', + }); + + public readonly isConfigValid = (config: Config): config is Config => { + return !!config.name; + }; + + public readonly execute = async (config: Config, context: RangeSelectContext) => { + alert(`Hello, ${config.name}, your selected range: ${JSON.stringify(context.data.range)}`); + }; +} diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx index ba88f49861ffe..ba8d7f395e738 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/drilldown.tsx @@ -13,6 +13,7 @@ import { CollectConfigContainer } from './collect_config_container'; import { SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN } from './constants'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { txtGoToDiscover } from './i18n'; +import { APPLY_FILTER_TRIGGER } from '../../../../../src/plugins/ui_actions/public'; const isOutputWithIndexPatterns = ( output: unknown @@ -25,7 +26,8 @@ export interface Params { start: StartServicesGetter>; } -export class DashboardToDiscoverDrilldown implements Drilldown { +export class DashboardToDiscoverDrilldown + implements Drilldown { constructor(protected readonly params: Params) {} public readonly id = SAMPLE_DASHBOARD_TO_DISCOVER_DRILLDOWN; @@ -36,6 +38,10 @@ export class DashboardToDiscoverDrilldown implements Drilldown { + return [APPLY_FILTER_TRIGGER]; + } + private readonly ReactCollectConfig: React.FC = (props) => ( ); diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts index d8147827ed473..a10e8ad707e97 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_discover_drilldown/types.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; +import { ApplyGlobalFilterActionContext } from '../../../../../src/plugins/data/public'; -export type ActionContext = ChartActionContext; +export type ActionContext = ApplyGlobalFilterActionContext; export interface Config { /** diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx index 67599687dd881..7d915ea23c66f 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx +++ b/x-pack/examples/ui_actions_enhanced_examples/public/dashboard_to_url_drilldown/index.tsx @@ -5,11 +5,15 @@ */ import React from 'react'; -import { EuiFormRow, EuiSwitch, EuiFieldText, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiFieldText, EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../src/plugins/kibana_react/public'; import { UiActionsEnhancedDrilldownDefinition as Drilldown } from '../../../../plugins/ui_actions_enhanced/public'; import { ChartActionContext } from '../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../src/plugins/kibana_utils/public'; +import { + SELECT_RANGE_TRIGGER, + VALUE_CLICK_TRIGGER, +} from '../../../../../src/plugins/ui_actions/public'; import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public'; function isValidUrl(url: string) { @@ -28,11 +32,13 @@ export interface Config { openInNewTab: boolean; } -export type CollectConfigProps = CollectConfigPropsBase; +type UrlTrigger = typeof VALUE_CLICK_TRIGGER | typeof SELECT_RANGE_TRIGGER; + +export type CollectConfigProps = CollectConfigPropsBase; const SAMPLE_DASHBOARD_TO_URL_DRILLDOWN = 'SAMPLE_DASHBOARD_TO_URL_DRILLDOWN'; -export class DashboardToUrlDrilldown implements Drilldown { +export class DashboardToUrlDrilldown implements Drilldown { public readonly id = SAMPLE_DASHBOARD_TO_URL_DRILLDOWN; public readonly order = 8; @@ -43,7 +49,15 @@ export class DashboardToUrlDrilldown implements Drilldown public readonly euiIcon = 'link'; - private readonly ReactCollectConfig: React.FC = ({ config, onConfig }) => ( + supportedTriggers(): UrlTrigger[] { + return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER]; + } + + private readonly ReactCollectConfig: React.FC = ({ + config, + onConfig, + context, + }) => ( <>

@@ -79,6 +93,11 @@ export class DashboardToUrlDrilldown implements Drilldown onChange={() => onConfig({ ...config, openInNewTab: !config.openInNewTab })} /> + + + {/* just demo how can access selected triggers*/} +

Will be attached to triggers: {JSON.stringify(context.triggers)}

+
); diff --git a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts index 8034c378cc64f..7f2c9a9b3bbc8 100644 --- a/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts +++ b/x-pack/examples/ui_actions_enhanced_examples/public/plugin.ts @@ -15,6 +15,7 @@ import { DashboardToUrlDrilldown } from './dashboard_to_url_drilldown'; import { DashboardToDiscoverDrilldown } from './dashboard_to_discover_drilldown'; import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; import { DiscoverSetup, DiscoverStart } from '../../../../src/plugins/discover/public'; +import { DashboardHelloWorldOnlyRangeSelectDrilldown } from './dashboard_hello_world_only_range_select_drilldown'; export interface SetupDependencies { data: DataPublicPluginSetup; @@ -37,6 +38,7 @@ export class UiActionsEnhancedExamplesPlugin const start = createStartServicesGetter(core.getStartServices); uiActions.registerDrilldown(new DashboardHelloWorldDrilldown()); + uiActions.registerDrilldown(new DashboardHelloWorldOnlyRangeSelectDrilldown()); uiActions.registerDrilldown(new DashboardToUrlDrilldown()); uiActions.registerDrilldown(new DashboardToDiscoverDrilldown({ start })); } diff --git a/x-pack/package.json b/x-pack/package.json index 42fa74a3bc84e..57a0b88f8c2a5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -44,9 +44,9 @@ "@storybook/addon-storyshots": "^5.3.19", "@storybook/react": "^5.3.19", "@storybook/theming": "^5.3.19", + "@testing-library/jest-dom": "^5.8.0", "@testing-library/react": "^9.3.2", "@testing-library/react-hooks": "^3.2.1", - "@testing-library/jest-dom": "^5.8.0", "@types/angular": "^1.6.56", "@types/archiver": "^3.1.0", "@types/base64-js": "^1.2.5", @@ -72,8 +72,9 @@ "@types/gulp": "^4.0.6", "@types/hapi__wreck": "^15.0.1", "@types/he": "^1.1.1", - "@types/hoist-non-react-statics": "^3.3.1", "@types/history": "^4.7.3", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/http-proxy": "^1.17.4", "@types/jest": "^25.2.3", "@types/jest-specific-snapshot": "^0.5.4", "@types/joi": "^13.4.2", @@ -94,6 +95,7 @@ "@types/object-hash": "^1.3.0", "@types/papaparse": "^5.0.3", "@types/pngjs": "^3.3.2", + "@types/pretty-ms": "^5.0.0", "@types/prop-types": "^15.5.3", "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", @@ -109,6 +111,7 @@ "@types/redux-actions": "^2.6.1", "@types/set-value": "^2.0.0", "@types/sinon": "^7.0.13", + "@types/stats-lite": "^2.2.0", "@types/styled-components": "^5.1.0", "@types/supertest": "^2.0.5", "@types/tar-fs": "^1.16.1", @@ -116,11 +119,9 @@ "@types/tinycolor2": "^1.4.1", "@types/use-resize-observer": "^6.0.0", "@types/uuid": "^3.4.4", + "@types/webpack-env": "^1.15.2", "@types/xml-crypto": "^1.4.0", "@types/xml2js": "^0.4.5", - "@types/stats-lite": "^2.2.0", - "@types/pretty-ms": "^5.0.0", - "@types/webpack-env": "^1.15.2", "@welldone-software/why-did-you-render": "^4.0.0", "abab": "^1.0.4", "autoprefixer": "^9.7.4", @@ -227,6 +228,7 @@ "@turf/circle": "6.0.1", "@turf/distance": "6.0.1", "@turf/helpers": "6.0.1", + "@types/http-proxy-agent": "^2.0.2", "angular": "^1.8.0", "angular-resource": "1.8.0", "angular-sanitize": "1.8.0", diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts index de96864d0b295..1030e3d9c5d8e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -9,6 +9,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { TypeOf } from '@kbn/config-schema'; +import { Logger } from '../../../../../../src/core/server'; import { ExternalIncidentServiceConfigurationSchema, @@ -122,7 +123,12 @@ export interface ExternalServiceApi { export interface CreateExternalServiceBasicArgs { api: ExternalServiceApi; - createExternalService: (credentials: ExternalServiceCredentials) => ExternalService; + createExternalService: ( + credentials: ExternalServiceCredentials, + logger: Logger, + proxySettings?: any + ) => ExternalService; + logger: Logger; } export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index 82dedb09c429e..d895bf386a367 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -67,6 +67,7 @@ export const mapParams = ( export const createConnectorExecutor = ({ api, createExternalService, + logger, }: CreateExternalServiceBasicArgs) => async ( execOptions: ActionTypeExecutorOptions< ExternalIncidentServiceConfiguration, @@ -83,10 +84,14 @@ export const createConnectorExecutor = ({ actionId, }; - const externalService = createExternalService({ - config, - secrets, - }); + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + execOptions.proxySettings + ); if (!api[subAction]) { throw new Error('[Action][ExternalService] Unsupported subAction type.'); @@ -122,10 +127,11 @@ export const createConnector = ({ validate, createExternalService, validationSchema, + logger, }: CreateExternalServiceArgs) => { return ({ configurationUtilities, - executor = createConnectorExecutor({ api, createExternalService }), + executor = createConnectorExecutor({ api, createExternalService, logger }), }: CreateActionTypeArgs): ActionType => ({ ...config, validate: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 195f6db538ae5..62f369816d714 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -269,6 +269,7 @@ describe('execute()', () => { "message": "a message to you", "subject": "the subject", }, + "proxySettings": undefined, "routing": Object { "bcc": Array [ "jimmy@example.com", @@ -326,6 +327,7 @@ describe('execute()', () => { "message": "a message to you", "subject": "the subject", }, + "proxySettings": undefined, "routing": Object { "bcc": Array [ "jimmy@example.com", diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index a51a0432a01e0..e9dc4eea5dcfc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -184,6 +184,7 @@ async function executor( subject: params.subject, message: params.message, }, + proxySettings: execOptions.proxySettings, }; let result; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 80a171cbe624d..3591e05fb3acf 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -31,9 +31,9 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServerLogActionType({ logger })); - actionTypeRegistry.register(getSlackActionType({ configurationUtilities })); + actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities })); - actionTypeRegistry.register(getJiraActionType({ configurationUtilities })); - actionTypeRegistry.register(getResilientActionType({ configurationUtilities })); + actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities })); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index a2d7bb5930a75..66be0bad02d7b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -4,21 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../../src/core/server'; import { createConnector } from '../case/utils'; +import { ActionType } from '../../types'; import { api } from './api'; import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; -export const getActionType = createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: JiraPublicConfiguration, - secrets: JiraSecretConfiguration, - }, -}); +export function getActionType({ + logger, + configurationUtilities, +}: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +}): ActionType { + return createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: JiraPublicConfiguration, + secrets: JiraSecretConfiguration, + }, + logger, + })({ configurationUtilities }); +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 3de3926b7d821..547595b4c183f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -9,6 +9,9 @@ import axios from 'axios'; import { createExternalService } from './service'; import * as utils from '../lib/axios_utils'; import { ExternalService } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); jest.mock('../lib/axios_utils', () => { @@ -26,10 +29,13 @@ describe('Jira service', () => { let service: ExternalService; beforeAll(() => { - service = createExternalService({ - config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, - secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, - }); + service = createExternalService( + { + config: { apiUrl: 'https://siem-kibana.atlassian.net', projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }, + logger + ); }); beforeEach(() => { @@ -39,37 +45,49 @@ describe('Jira service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ - config: { apiUrl: null, projectKey: 'CK' }, - secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, - }) + createExternalService( + { + config: { apiUrl: null, projectKey: 'CK' }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }, + logger + ) ).toThrow(); }); test('throws without projectKey', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', projectKey: null }, - secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', projectKey: null }, + secrets: { apiToken: 'token', email: 'elastic@elastic.com' }, + }, + logger + ) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: 'elastic@elastic.com' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: 'elastic@elastic.com' }, + }, + logger + ) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: undefined }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { apiToken: '', email: undefined }, + }, + logger + ) ).toThrow(); }); }); @@ -92,6 +110,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', + logger, }); }); @@ -146,6 +165,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + logger, method: 'post', data: { fields: { @@ -210,6 +230,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, method: 'put', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', data: { fields: { summary: 'title', description: 'desc' } }, @@ -272,6 +293,7 @@ describe('Jira service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, method: 'post', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1/comment', data: { body: 'comment' }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index 240b645c3a7dc..aec73cfb375ed 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -7,6 +7,7 @@ import axios from 'axios'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; import { JiraPublicConfigurationType, JiraSecretConfigurationType, @@ -17,6 +18,7 @@ import { import * as i18n from './translations'; import { request, getErrorMessage } from '../lib/axios_utils'; +import { ProxySettings } from '../../types'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; @@ -25,10 +27,11 @@ const COMMENT_URL = `comment`; const VIEW_INCIDENT_URL = `browse`; -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredentials): ExternalService => { +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + proxySettings?: ProxySettings +): ExternalService => { const { apiUrl: url, projectKey } = config as JiraPublicConfigurationType; const { apiToken, email } = secrets as JiraSecretConfigurationType; @@ -55,6 +58,8 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}/${id}`, + logger, + proxySettings, }); const { fields, ...rest } = res.data; @@ -75,10 +80,12 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, + logger, method: 'post', data: { fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } }, }, + proxySettings, }); const updatedIncident = await getIncident(res.data.id); @@ -102,7 +109,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'put', url: `${incidentUrl}/${incidentId}`, + logger, data: { fields: { ...incident } }, + proxySettings, }); const updatedIncident = await getIncident(incidentId); @@ -129,7 +138,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), + logger, data: { body: comment.comment }, + proxySettings, }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 4a52ae60bcdda..844aa6d2de7ed 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -5,7 +5,11 @@ */ import axios from 'axios'; -import { addTimeZoneToDate, throwIfNotAlive, request, patch, getErrorMessage } from './axios_utils'; +import HttpProxyAgent from 'http-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { addTimeZoneToDate, request, patch, getErrorMessage } from './axios_utils'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); const axiosMock = (axios as unknown) as jest.Mock; @@ -21,26 +25,6 @@ describe('addTimeZoneToDate', () => { }); }); -describe('throwIfNotAlive ', () => { - test('throws correctly when status is invalid', async () => { - expect(() => { - throwIfNotAlive(404, 'application/json'); - }).toThrow('Instance is not alive.'); - }); - - test('throws correctly when content is invalid', () => { - expect(() => { - throwIfNotAlive(200, 'application/html'); - }).toThrow('Instance is not alive.'); - }); - - test('do NOT throws with custom validStatusCodes', async () => { - expect(() => { - throwIfNotAlive(404, 'application/json', [404]); - }).not.toThrow('Instance is not alive.'); - }); -}); - describe('request', () => { beforeEach(() => { axiosMock.mockImplementation(() => ({ @@ -51,9 +35,22 @@ describe('request', () => { }); test('it fetch correctly with defaults', async () => { - const res = await request({ axios, url: '/test' }); + const res = await request({ + axios, + url: '/test', + logger, + }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {} }); + expect(axiosMock).toHaveBeenCalledWith('/test', { + method: 'get', + data: {}, + headers: undefined, + httpAgent: undefined, + httpsAgent: undefined, + params: undefined, + proxy: false, + validateStatus: undefined, + }); expect(res).toEqual({ status: 200, headers: { 'content-type': 'application/json' }, @@ -61,10 +58,27 @@ describe('request', () => { }); }); - test('it fetch correctly', async () => { - const res = await request({ axios, url: '/test', method: 'post', data: { id: '123' } }); + test('it have been called with proper proxy agent', async () => { + const res = await request({ + axios, + url: '/testProxy', + logger, + proxySettings: { + proxyUrl: 'http://localhost:1212', + rejectUnauthorizedCertificates: false, + }, + }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' } }); + expect(axiosMock).toHaveBeenCalledWith('/testProxy', { + method: 'get', + data: {}, + headers: undefined, + httpAgent: new HttpProxyAgent('http://localhost:1212'), + httpsAgent: new HttpProxyAgent('http://localhost:1212'), + params: undefined, + proxy: false, + validateStatus: undefined, + }); expect(res).toEqual({ status: 200, headers: { 'content-type': 'application/json' }, @@ -72,14 +86,24 @@ describe('request', () => { }); }); - test('it throws correctly', async () => { - axiosMock.mockImplementation(() => ({ - status: 404, + test('it fetch correctly', async () => { + const res = await request({ axios, url: '/test', method: 'post', logger, data: { id: '123' } }); + + expect(axiosMock).toHaveBeenCalledWith('/test', { + method: 'post', + data: { id: '123' }, + headers: undefined, + httpAgent: undefined, + httpsAgent: undefined, + params: undefined, + proxy: false, + validateStatus: undefined, + }); + expect(res).toEqual({ + status: 200, headers: { 'content-type': 'application/json' }, data: { incidentId: '123' }, - })); - - await expect(request({ axios, url: '/test' })).rejects.toThrow(); + }); }); }); @@ -92,8 +116,17 @@ describe('patch', () => { }); test('it fetch correctly', async () => { - await patch({ axios, url: '/test', data: { id: '123' } }); - expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' } }); + await patch({ axios, url: '/test', data: { id: '123' }, logger }); + expect(axiosMock).toHaveBeenCalledWith('/test', { + method: 'patch', + data: { id: '123' }, + headers: undefined, + httpAgent: undefined, + httpsAgent: undefined, + params: undefined, + proxy: false, + validateStatus: undefined, + }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index d527cf632bace..e26a3b686179c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -4,50 +4,68 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AxiosInstance, Method, AxiosResponse } from 'axios'; - -export const throwIfNotAlive = ( - status: number, - contentType: string, - validStatusCodes: number[] = [200, 201, 204] -) => { - if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { - throw new Error('Instance is not alive.'); - } -}; +import { AxiosInstance, Method, AxiosResponse, AxiosBasicCredentials } from 'axios'; +import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; +import { getProxyAgent } from './get_proxy_agent'; export const request = async ({ axios, url, + logger, method = 'get', data, params, + proxySettings, + headers, + validateStatus, + auth, }: { axios: AxiosInstance; url: string; + logger: Logger; method?: Method; data?: T; params?: unknown; + proxySettings?: ProxySettings; + headers?: Record | null; + validateStatus?: (status: number) => boolean; + auth?: AxiosBasicCredentials; }): Promise => { - const res = await axios(url, { method, data: data ?? {}, params }); - throwIfNotAlive(res.status, res.headers['content-type']); - return res; + return await axios(url, { + method, + data: data ?? {}, + params, + auth, + // use httpsAgent and embedded proxy: false, to be able to handle fail on invalid certs + httpsAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined, + httpAgent: proxySettings ? getProxyAgent(proxySettings, logger) : undefined, + proxy: false, // the same way as it done for IncomingWebhook in + headers, + validateStatus, + }); }; export const patch = async ({ axios, url, data, + logger, + proxySettings, }: { axios: AxiosInstance; url: string; data: T; + logger: Logger; + proxySettings?: ProxySettings; }): Promise => { return request({ axios, url, + logger, method: 'patch', data, + proxySettings, }); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts new file mode 100644 index 0000000000000..2468fab8c6ac5 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { getProxyAgent } from './get_proxy_agent'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; + +describe('getProxyAgent', () => { + test('return HttpsProxyAgent for https proxy url', () => { + const agent = getProxyAgent( + { proxyUrl: 'https://someproxyhost', rejectUnauthorizedCertificates: false }, + logger + ); + expect(agent instanceof HttpsProxyAgent).toBeTruthy(); + }); + + test('return HttpProxyAgent for http proxy url', () => { + const agent = getProxyAgent( + { proxyUrl: 'http://someproxyhost', rejectUnauthorizedCertificates: false }, + logger + ); + expect(agent instanceof HttpProxyAgent).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts new file mode 100644 index 0000000000000..bb4dadd3a4698 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_proxy_agent.ts @@ -0,0 +1,31 @@ +/* + * 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 HttpProxyAgent from 'http-proxy-agent'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; + +export function getProxyAgent( + proxySettings: ProxySettings, + logger: Logger +): HttpsProxyAgent | HttpProxyAgent { + logger.debug(`Create proxy agent for ${proxySettings.proxyUrl}.`); + + if (/^https/i.test(proxySettings.proxyUrl)) { + const proxyUrl = new URL(proxySettings.proxyUrl); + return new HttpsProxyAgent({ + host: proxyUrl.hostname, + port: Number(proxyUrl.port), + protocol: proxyUrl.protocol, + headers: proxySettings.proxyHeaders, + // do not fail on invalid certs if value is false + rejectUnauthorized: proxySettings.rejectUnauthorizedCertificates, + }); + } else { + return new HttpProxyAgent(proxySettings.proxyUrl); + } +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts index 92f88ebe0be22..d78237beb98a1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts @@ -5,22 +5,34 @@ */ import axios, { AxiosResponse } from 'axios'; -import { Services } from '../../types'; +import { Logger } from '../../../../../../src/core/server'; +import { Services, ProxySettings } from '../../types'; +import { request } from './axios_utils'; interface PostPagerdutyOptions { apiUrl: string; data: unknown; headers: Record; services: Services; + proxySettings?: ProxySettings; } // post an event to pagerduty -export async function postPagerduty(options: PostPagerdutyOptions): Promise { - const { apiUrl, data, headers } = options; - const axiosOptions = { +export async function postPagerduty( + options: PostPagerdutyOptions, + logger: Logger +): Promise { + const { apiUrl, data, headers, proxySettings } = options; + const axiosInstance = axios.create(); + + return await request({ + axios: axiosInstance, + url: apiUrl, + method: 'post', + logger, + data, + proxySettings, headers, validateStatus: () => true, - }; - - return axios.post(apiUrl, data, axiosOptions); + }); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 3514bd4257b0f..8287ee944bca9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -12,6 +12,7 @@ import { Logger } from '../../../../../../src/core/server'; import { sendEmail } from './send_email'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import nodemailer from 'nodemailer'; +import { ProxySettings } from '../../types'; const createTransportMock = nodemailer.createTransport as jest.Mock; const sendMailMockResult = { result: 'does not matter' }; @@ -63,6 +64,59 @@ describe('send_email module', () => { }); test('handles unauthenticated email using not secure host/port', async () => { + const sendEmailOptions = getSendEmailOptions( + { + transport: { + host: 'example.com', + port: 1025, + }, + }, + { + proxyUrl: 'https://example.com', + rejectUnauthorizedCertificates: false, + } + ); + delete sendEmailOptions.transport.service; + delete sendEmailOptions.transport.user; + delete sendEmailOptions.transport.password; + const result = await sendEmail(mockLogger, sendEmailOptions); + expect(result).toBe(sendMailMockResult); + expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "headers": undefined, + "host": "example.com", + "port": 1025, + "proxy": "https://example.com", + "secure": false, + "tls": Object { + "rejectUnauthorized": false, + }, + }, + ] + `); + expect(sendMailMock.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "bcc": Array [], + "cc": Array [ + "bob@example.com", + "robert@example.com", + ], + "from": "fred@example.com", + "html": "

a message

+ ", + "subject": "a subject", + "text": "a message", + "to": Array [ + "jim@example.com", + ], + }, + ] + `); + }); + + test('rejectUnauthorized default setting email using not secure host/port', async () => { const sendEmailOptions = getSendEmailOptions({ transport: { host: 'example.com', @@ -80,9 +134,6 @@ describe('send_email module', () => { "host": "example.com", "port": 1025, "secure": false, - "tls": Object { - "rejectUnauthorized": false, - }, }, ] `); @@ -161,7 +212,10 @@ describe('send_email module', () => { }); }); -function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {}) { +function getSendEmailOptions( + { content = {}, routing = {}, transport = {} } = {}, + proxySettings?: ProxySettings +) { return { content: { ...content, @@ -181,5 +235,6 @@ function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {} user: 'elastic', password: 'changeme', }, + proxySettings, }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index 869db34f034ae..a4f32f1880cb5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -6,10 +6,10 @@ // info on nodemailer: https://nodemailer.com/about/ import nodemailer from 'nodemailer'; - import { default as MarkdownIt } from 'markdown-it'; import { Logger } from '../../../../../../src/core/server'; +import { ProxySettings } from '../../types'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -18,6 +18,7 @@ export interface SendEmailOptions { transport: Transport; routing: Routing; content: Content; + proxySettings?: ProxySettings; } // config validation ensures either service is set or host/port are set @@ -44,7 +45,7 @@ export interface Content { // send an email export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { - const { transport, routing, content } = options; + const { transport, routing, content, proxySettings } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; const { subject, message } = content; @@ -67,11 +68,16 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom transportConfig.host = host; transportConfig.port = port; transportConfig.secure = !!secure; - if (!transportConfig.secure) { + if (proxySettings && !transportConfig.secure) { transportConfig.tls = { - rejectUnauthorized: false, + // do not fail on invalid certs if value is false + rejectUnauthorized: proxySettings?.rejectUnauthorizedCertificates, }; } + if (proxySettings) { + transportConfig.proxy = proxySettings.proxyUrl; + transportConfig.headers = proxySettings.proxyHeaders; + } } const nodemailerTransport = nodemailer.createTransport(transportConfig); diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index b76e57419bc56..c0edfc530e738 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -161,6 +161,7 @@ async function executor( const secrets = execOptions.secrets; const params = execOptions.params; const services = execOptions.services; + const proxySettings = execOptions.proxySettings; const apiUrl = getPagerDutyApiUrl(config); const headers = { @@ -171,7 +172,7 @@ async function executor( let response; try { - response = await postPagerduty({ apiUrl, data, headers, services }); + response = await postPagerduty({ apiUrl, data, headers, services, proxySettings }, logger); } catch (err) { const message = i18n.translate('xpack.actions.builtin.pagerduty.postingErrorMessage', { defaultMessage: 'error posting pagerduty event', diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index e98bc71559d3f..1e9cb15589702 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../../src/core/server'; import { createConnector } from '../case/utils'; import { api } from './api'; @@ -11,14 +12,25 @@ import { config } from './config'; import { validate } from './validators'; import { createExternalService } from './service'; import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ActionType } from '../../types'; -export const getActionType = createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: ResilientPublicConfiguration, - secrets: ResilientSecretConfiguration, - }, -}); +export function getActionType({ + logger, + configurationUtilities, +}: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +}): ActionType { + return createConnector({ + api, + config, + validate, + createExternalService, + validationSchema: { + config: ResilientPublicConfiguration, + secrets: ResilientSecretConfiguration, + }, + logger, + })({ configurationUtilities }); +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index 573885698014e..a9271671f68b9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -9,6 +9,9 @@ import axios from 'axios'; import { createExternalService, getValueTextContent, formatUpdateRequest } from './service'; import * as utils from '../lib/axios_utils'; import { ExternalService } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); jest.mock('../lib/axios_utils', () => { @@ -72,10 +75,13 @@ describe('IBM Resilient service', () => { let service: ExternalService; beforeAll(() => { - service = createExternalService({ - config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' }, - secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' }, - }); + service = createExternalService( + { + config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' }, + secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' }, + }, + logger + ); }); afterAll(() => { @@ -138,37 +144,49 @@ describe('IBM Resilient service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ - config: { apiUrl: null, orgId: '201' }, - secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, - }) + createExternalService( + { + config: { apiUrl: null, orgId: '201' }, + secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, + }, + logger + ) ).toThrow(); }); test('throws without orgId', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', orgId: null }, - secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', orgId: null }, + secrets: { apiKeyId: 'token', apiKeySecret: 'secret' }, + }, + logger + ) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', orgId: '201' }, - secrets: { apiKeyId: '', apiKeySecret: 'secret' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', orgId: '201' }, + secrets: { apiKeyId: '', apiKeySecret: 'secret' }, + }, + logger + ) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com', orgId: '201' }, - secrets: { apiKeyId: '', apiKeySecret: undefined }, - }) + createExternalService( + { + config: { apiUrl: 'test.com', orgId: '201' }, + secrets: { apiKeyId: '', apiKeySecret: undefined }, + }, + logger + ) ).toThrow(); }); }); @@ -197,6 +215,7 @@ describe('IBM Resilient service', () => { await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1', params: { text_content_output_format: 'objects_convert', @@ -256,6 +275,7 @@ describe('IBM Resilient service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, url: 'https://resilient.elastic.co/rest/orgs/201/incidents', + logger, method: 'post', data: { name: 'title', @@ -311,6 +331,7 @@ describe('IBM Resilient service', () => { // The second call to the API is the update call. expect(requestMock.mock.calls[1][0]).toEqual({ axios, + logger, method: 'patch', url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1', data: { @@ -392,7 +413,9 @@ describe('IBM Resilient service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, method: 'post', + proxySettings: undefined, url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments', data: { text: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts index 8d0526ca3b571..b2150081f2c89 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts @@ -6,6 +6,7 @@ import axios from 'axios'; +import { Logger } from '../../../../../../src/core/server'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { ResilientPublicConfigurationType, @@ -19,6 +20,7 @@ import { import * as i18n from './translations'; import { getErrorMessage, request } from '../lib/axios_utils'; +import { ProxySettings } from '../../types'; const BASE_URL = `rest`; const INCIDENT_URL = `incidents`; @@ -57,10 +59,11 @@ export const formatUpdateRequest = ({ }; }; -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredentials): ExternalService => { +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + proxySettings?: ProxySettings +): ExternalService => { const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType; const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType; @@ -88,9 +91,11 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}/${id}`, + logger, params: { text_content_output_format: 'objects_convert', }, + proxySettings, }); return { ...res.data, description: res.data.description?.content ?? '' }; @@ -107,6 +112,7 @@ export const createExternalService = ({ axios: axiosInstance, url: `${incidentUrl}`, method: 'post', + logger, data: { ...incident, description: { @@ -115,6 +121,7 @@ export const createExternalService = ({ }, discovered_date: Date.now(), }, + proxySettings, }); return { @@ -139,7 +146,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'patch', url: `${incidentUrl}/${incidentId}`, + logger, data, + proxySettings, }); if (!res.data.success) { @@ -170,7 +179,9 @@ export const createExternalService = ({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), + logger, data: { text: { format: 'text', content: comment.comment } }, + proxySettings, }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 109008b8fc9fb..3addbe7c54dac 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -76,10 +76,14 @@ async function executor( const { subAction, subActionParams } = params; let data: PushToServiceResponse | null = null; - const externalService = createExternalService({ - config, - secrets, - }); + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + execOptions.proxySettings + ); if (!api[subAction]) { const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index 07d60ec9f7a05..2adcdf561ce17 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -9,6 +9,9 @@ import axios from 'axios'; import { createExternalService } from './service'; import * as utils from '../lib/axios_utils'; import { ExternalService } from './types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); jest.mock('../lib/axios_utils', () => { @@ -28,10 +31,13 @@ describe('ServiceNow service', () => { let service: ExternalService; beforeAll(() => { - service = createExternalService({ - config: { apiUrl: 'https://dev102283.service-now.com' }, - secrets: { username: 'admin', password: 'admin' }, - }); + service = createExternalService( + { + config: { apiUrl: 'https://dev102283.service-now.com' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger + ); }); beforeEach(() => { @@ -41,28 +47,37 @@ describe('ServiceNow service', () => { describe('createExternalService', () => { test('throws without url', () => { expect(() => - createExternalService({ - config: { apiUrl: null }, - secrets: { username: 'admin', password: 'admin' }, - }) + createExternalService( + { + config: { apiUrl: null }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger + ) ).toThrow(); }); test('throws without username', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { username: '', password: 'admin' }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: 'admin' }, + }, + logger + ) ).toThrow(); }); test('throws without password', () => { expect(() => - createExternalService({ - config: { apiUrl: 'test.com' }, - secrets: { username: '', password: undefined }, - }) + createExternalService( + { + config: { apiUrl: 'test.com' }, + secrets: { username: '', password: undefined }, + }, + logger + ) ).toThrow(); }); }); @@ -84,6 +99,7 @@ describe('ServiceNow service', () => { await service.getIncident('1'); expect(requestMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', }); }); @@ -127,6 +143,7 @@ describe('ServiceNow service', () => { expect(requestMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://dev102283.service-now.com/api/now/v2/table/incident', method: 'post', data: { short_description: 'title', description: 'desc' }, @@ -179,6 +196,7 @@ describe('ServiceNow service', () => { expect(patchMock).toHaveBeenCalledWith({ axios, + logger, url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', data: { short_description: 'title', description: 'desc' }, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 2b5204af2eb7d..cf1c26e6462a2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -9,8 +9,10 @@ import axios from 'axios'; import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; import * as i18n from './translations'; +import { Logger } from '../../../../../../src/core/server'; import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils'; +import { ProxySettings } from '../../types'; const API_VERSION = 'v2'; const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; @@ -18,10 +20,11 @@ const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; -export const createExternalService = ({ - config, - secrets, -}: ExternalServiceCredentials): ExternalService => { +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + proxySettings?: ProxySettings +): ExternalService => { const { apiUrl: url } = config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; @@ -43,6 +46,8 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: `${incidentUrl}/${id}`, + logger, + proxySettings, }); return { ...res.data.result }; @@ -58,6 +63,8 @@ export const createExternalService = ({ const res = await request({ axios: axiosInstance, url: incidentUrl, + logger, + proxySettings, params, }); @@ -71,9 +78,13 @@ export const createExternalService = ({ const createIncident = async ({ incident }: ExternalServiceParams) => { try { + logger.warn(`incident error : ${JSON.stringify(proxySettings)}`); + logger.warn(`incident error : ${url}`); const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, + logger, + proxySettings, method: 'post', data: { ...(incident as Record) }, }); @@ -96,7 +107,9 @@ export const createExternalService = ({ const res = await patch({ axios: axiosInstance, url: `${incidentUrl}/${incidentId}`, + logger, data: { ...(incident as Record) }, + proxySettings, }); return { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 6d4176067c3ba..812657138152c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -4,25 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Logger } from '../../../../../src/core/server'; import { Services, ActionTypeExecutorResult } from '../types'; import { validateParams, validateSecrets } from '../lib'; import { getActionType, SlackActionType, SlackActionTypeExecutorOptions } from './slack'; import { actionsConfigMock } from '../actions_config.mock'; import { actionsMock } from '../mocks'; +import { createActionTypeRegistry } from './index.test'; + +jest.mock('@slack/webhook', () => { + return { + IncomingWebhook: jest.fn().mockImplementation(() => { + return { send: (message: string) => {} }; + }), + }; +}); const ACTION_TYPE_ID = '.slack'; const services: Services = actionsMock.createServices(); let actionType: SlackActionType; +let mockedLogger: jest.Mocked; beforeAll(() => { + const { logger } = createActionTypeRegistry(); actionType = getActionType({ async executor(options) { return { status: 'ok', actionId: options.actionId }; }, configurationUtilities: actionsConfigMock.create(), + logger, }); + mockedLogger = logger; + expect(actionType).toBeTruthy(); }); describe('action registeration', () => { @@ -83,6 +98,7 @@ describe('validateActionTypeSecrets()', () => { test('should validate and pass when the slack webhookUrl is whitelisted', () => { actionType = getActionType({ + logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), ensureWhitelistedUri: (url) => { @@ -98,9 +114,10 @@ describe('validateActionTypeSecrets()', () => { test('config validation returns an error if the specified URL isnt whitelisted', () => { actionType = getActionType({ + logger: mockedLogger, configurationUtilities: { ...actionsConfigMock.create(), - ensureWhitelistedHostname: (url) => { + ensureWhitelistedHostname: () => { throw new Error(`target hostname is not whitelisted`); }, }, @@ -136,6 +153,7 @@ describe('execute()', () => { actionType = getActionType({ executor: mockSlackExecutor, + logger: mockedLogger, configurationUtilities: actionsConfigMock.create(), }); }); @@ -147,6 +165,10 @@ describe('execute()', () => { config: {}, secrets: { webhookUrl: 'http://example.com' }, params: { message: 'this invocation should succeed' }, + proxySettings: { + proxyUrl: 'https://someproxyhost', + rejectUnauthorizedCertificates: false, + }, }); expect(response).toMatchInlineSnapshot(` Object { @@ -170,4 +192,25 @@ describe('execute()', () => { `"slack mockExecutor failure: this invocation should fail"` ); }); + + test('calls the mock executor with success proxy', async () => { + const actionTypeProxy = getActionType({ + logger: mockedLogger, + configurationUtilities: actionsConfigMock.create(), + }); + await actionTypeProxy.executor({ + actionId: 'some-id', + services, + config: {}, + secrets: { webhookUrl: 'http://example.com' }, + params: { message: 'this invocation should succeed' }, + proxySettings: { + proxyUrl: 'https://someproxyhost', + rejectUnauthorizedCertificates: false, + }, + }); + expect(mockedLogger.info).toHaveBeenCalledWith( + 'IncomingWebhook was called with proxyUrl https://someproxyhost' + ); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index 209582585256b..293328c809435 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -6,11 +6,14 @@ import { URL } from 'url'; import { curry } from 'lodash'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import HttpProxyAgent from 'http-proxy-agent'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; import { IncomingWebhook, IncomingWebhookResult } from '@slack/webhook'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, getOrElse } from 'fp-ts/lib/Option'; +import { Logger } from '../../../../../src/core/server'; import { getRetryAfterIntervalFromHeaders } from './lib/http_rersponse_retry_header'; import { @@ -20,6 +23,7 @@ import { ExecutorType, } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; +import { getProxyAgent } from './lib/get_proxy_agent'; export type SlackActionType = ActionType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; export type SlackActionTypeExecutorOptions = ActionTypeExecutorOptions< @@ -49,9 +53,11 @@ const ParamsSchema = schema.object({ // customizing executor is only used for tests export function getActionType({ + logger, configurationUtilities, - executor = slackExecutor, + executor = curry(slackExecutor)({ logger }), }: { + logger: Logger; configurationUtilities: ActionsConfigurationUtilities; executor?: ExecutorType<{}, ActionTypeSecretsType, ActionParamsType, unknown>; }): SlackActionType { @@ -99,6 +105,7 @@ function valdiateActionTypeConfig( // action executor async function slackExecutor( + { logger }: { logger: Logger }, execOptions: SlackActionTypeExecutorOptions ): Promise> { const actionId = execOptions.actionId; @@ -109,10 +116,22 @@ async function slackExecutor( const { webhookUrl } = secrets; const { message } = params; + let proxyAgent: HttpsProxyAgent | HttpProxyAgent | undefined; + if (execOptions.proxySettings) { + proxyAgent = getProxyAgent(execOptions.proxySettings, logger); + logger.info(`IncomingWebhook was called with proxyUrl ${execOptions.proxySettings.proxyUrl}`); + } + try { - const webhook = new IncomingWebhook(webhookUrl); + // https://slack.dev/node-slack-sdk/webhook + // node-slack-sdk use Axios inside :) + const webhook = new IncomingWebhook(webhookUrl, { + agent: proxyAgent, + }); result = await webhook.send(message); } catch (err) { + logger.error(`error on ${actionId} slack event: ${err.message}`); + if (err.original == null || err.original.response == null) { return serviceErrorResult(actionId, err.message); } @@ -143,6 +162,8 @@ async function slackExecutor( }, } ); + logger.error(`error on ${actionId} slack action: ${errMessage}`); + return errorResult(actionId, errMessage); } diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 26dd8a1a1402a..ea9f30452918c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -4,10 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('axios', () => ({ - request: jest.fn(), -})); - import { Services } from '../types'; import { validateConfig, validateSecrets, validateParams } from '../lib'; import { actionsConfigMock } from '../actions_config.mock'; @@ -24,7 +20,22 @@ import { WebhookMethods, } from './webhook'; -const axiosRequestMock = axios.request as jest.Mock; +import * as utils from './lib/axios_utils'; + +jest.mock('axios'); +jest.mock('./lib/axios_utils', () => { + const originalUtils = jest.requireActual('./lib/axios_utils'); + return { + ...originalUtils, + request: jest.fn(), + patch: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; + +axios.create = jest.fn(() => axios); const ACTION_TYPE_ID = '.webhook'; @@ -227,7 +238,7 @@ describe('params validation', () => { describe('execute()', () => { beforeAll(() => { - axiosRequestMock.mockReset(); + requestMock.mockReset(); actionType = getActionType({ logger: mockedLogger, configurationUtilities: actionsConfigMock.create(), @@ -235,8 +246,8 @@ describe('execute()', () => { }); beforeEach(() => { - axiosRequestMock.mockReset(); - axiosRequestMock.mockResolvedValue({ + requestMock.mockReset(); + requestMock.mockResolvedValue({ status: 200, statusText: '', data: '', @@ -261,17 +272,42 @@ describe('execute()', () => { params: { body: 'some data' }, }); - expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { "auth": Object { "password": "123", "username": "abc", }, + "axios": undefined, "data": "some data", "headers": Object { "aheader": "a value", }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from webhook action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, "method": "post", + "proxySettings": undefined, "url": "https://abc.def/my-webhook", } `); @@ -294,13 +330,38 @@ describe('execute()', () => { params: { body: 'some data' }, }); - expect(axiosRequestMock.mock.calls[0][0]).toMatchInlineSnapshot(` + expect(requestMock.mock.calls[0][0]).toMatchInlineSnapshot(` Object { + "axios": undefined, "data": "some data", "headers": Object { "aheader": "a value", }, + "logger": Object { + "context": Array [], + "debug": [MockFunction] { + "calls": Array [ + Array [ + "response from webhook action \\"some-id\\": [HTTP 200] ", + ], + ], + "results": Array [ + Object { + "type": "return", + "value": undefined, + }, + ], + }, + "error": [MockFunction], + "fatal": [MockFunction], + "get": [MockFunction], + "info": [MockFunction], + "log": [MockFunction], + "trace": [MockFunction], + "warn": [MockFunction], + }, "method": "post", + "proxySettings": undefined, "url": "https://abc.def/my-webhook", } `); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index be75742fa882e..d9a005565498d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -15,6 +15,7 @@ import { isOk, promiseResult, Result } from './lib/result_type'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../types'; import { ActionsConfigurationUtilities } from '../actions_config'; import { Logger } from '../../../../../src/core/server'; +import { request } from './lib/axios_utils'; // config definition export enum WebhookMethods { @@ -136,13 +137,18 @@ export async function executor( ? { auth: { username: secrets.user, password: secrets.password } } : {}; + const axiosInstance = axios.create(); + const result: Result = await promiseResult( - axios.request({ + request({ + axios: axiosInstance, method, url, + logger, ...basicAuth, headers, data, + proxySettings: execOptions.proxySettings, }) ); @@ -159,7 +165,7 @@ export async function executor( if (error.response) { const { status, statusText, headers: responseHeaders } = error.response; const message = `[${status}] ${statusText}`; - logger.warn(`error on ${actionId} webhook event: ${message}`); + logger.error(`error on ${actionId} webhook event: ${message}`); // The request was made and the server responded with a status code // that falls out of the range of 2xx // special handling for 5xx @@ -178,7 +184,7 @@ export async function executor( return errorResultInvalid(actionId, message); } - logger.warn(`error on ${actionId} webhook action: unexpected error`); + logger.error(`error on ${actionId} webhook action: unexpected error`); return errorResultUnexpectedError(actionId); } } diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index e86f2d7832828..795fbbf84145b 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -15,6 +15,7 @@ describe('config validation', () => { "*", ], "preconfigured": Object {}, + "rejectUnauthorizedCertificates": true, "whitelistedHosts": Array [ "*", ], @@ -33,6 +34,7 @@ describe('config validation', () => { }, }, }, + rejectUnauthorizedCertificates: false, }; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { @@ -50,6 +52,7 @@ describe('config validation', () => { "secrets": Object {}, }, }, + "rejectUnauthorizedCertificates": false, "whitelistedHosts": Array [ "*", ], diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index b2f3fa2680a9c..ba80915ebe243 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -32,6 +32,9 @@ export const configSchema = schema.object({ defaultValue: {}, validate: validatePreconfigured, }), + proxyUrl: schema.maybe(schema.string()), + proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), + rejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), }); export type ActionsConfig = TypeOf; diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index bce06c829b1bc..97c08124f5546 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -12,6 +12,7 @@ import { GetServicesFunction, RawAction, PreConfiguredAction, + ProxySettings, } from '../types'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { SpacesServiceSetup } from '../../../spaces/server'; @@ -28,6 +29,7 @@ export interface ActionExecutorContext { actionTypeRegistry: ActionTypeRegistryContract; eventLogger: IEventLogger; preconfiguredActions: PreConfiguredAction[]; + proxySettings?: ProxySettings; } export interface ExecuteOptions { @@ -78,6 +80,7 @@ export class ActionExecutor { eventLogger, preconfiguredActions, getActionsClientWithRequest, + proxySettings, } = this.actionExecutorContext!; const services = getServices(request); @@ -133,6 +136,7 @@ export class ActionExecutor { params: validatedParams, config: validatedConfig, secrets: validatedSecrets, + proxySettings, }); } catch (err) { rawResult = { diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index ca93e88d01203..341a17889923f 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -34,6 +34,7 @@ describe('Actions Plugin', () => { enabledActionTypes: ['*'], whitelistedHosts: ['*'], preconfigured: {}, + rejectUnauthorizedCertificates: true, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -194,6 +195,7 @@ describe('Actions Plugin', () => { secrets: {}, }, }, + rejectUnauthorizedCertificates: true, }); plugin = new ActionsPlugin(context); coreSetup = coreMock.createSetup(); @@ -217,7 +219,7 @@ describe('Actions Plugin', () => { // coreMock.createSetup doesn't support Plugin generics // eslint-disable-next-line @typescript-eslint/no-explicit-any await plugin.setup(coreSetup as any, pluginsSetup); - const pluginStart = plugin.start(coreStart, pluginsStart); + const pluginStart = await plugin.start(coreStart, pluginsStart); expect(pluginStart.isActionExecutable('preconfiguredServerLog', '.server-log')).toBe(true); }); @@ -232,7 +234,7 @@ describe('Actions Plugin', () => { usingEphemeralEncryptionKey: false, }, }); - const pluginStart = plugin.start(coreStart, pluginsStart); + const pluginStart = await plugin.start(coreStart, pluginsStart); await pluginStart.getActionsClientWithRequest(httpServerMock.createKibanaRequest()); }); @@ -241,7 +243,7 @@ describe('Actions Plugin', () => { // coreMock.createSetup doesn't support Plugin generics // eslint-disable-next-line @typescript-eslint/no-explicit-any await plugin.setup(coreSetup as any, pluginsSetup); - const pluginStart = plugin.start(coreStart, pluginsStart); + const pluginStart = await plugin.start(coreStart, pluginsStart); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); await expect( diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index ee50ee81d507c..413e6663105b8 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -116,6 +116,7 @@ export class ActionsPlugin implements Plugin, Plugi private readonly config: Promise; private readonly logger: Logger; + private actionsConfig?: ActionsConfig; private serverBasePath?: string; private taskRunnerFactory?: TaskRunnerFactory; private actionTypeRegistry?: ActionTypeRegistry; @@ -173,12 +174,12 @@ export class ActionsPlugin implements Plugin, Plugi // get executions count const taskRunnerFactory = new TaskRunnerFactory(actionExecutor); - const actionsConfig = (await this.config) as ActionsConfig; - const actionsConfigUtils = getActionsConfigurationUtilities(actionsConfig); + this.actionsConfig = (await this.config) as ActionsConfig; + const actionsConfigUtils = getActionsConfigurationUtilities(this.actionsConfig); - for (const preconfiguredId of Object.keys(actionsConfig.preconfigured)) { + for (const preconfiguredId of Object.keys(this.actionsConfig.preconfigured)) { this.preconfiguredActions.push({ - ...actionsConfig.preconfigured[preconfiguredId], + ...this.actionsConfig.preconfigured[preconfiguredId], id: preconfiguredId, isPreconfigured: true, }); @@ -317,6 +318,14 @@ export class ActionsPlugin implements Plugin, Plugi encryptedSavedObjectsClient, actionTypeRegistry: actionTypeRegistry!, preconfiguredActions, + proxySettings: + this.actionsConfig && this.actionsConfig.proxyUrl + ? { + proxyUrl: this.actionsConfig.proxyUrl, + proxyHeaders: this.actionsConfig.proxyHeaders, + rejectUnauthorizedCertificates: this.actionsConfig.rejectUnauthorizedCertificates, + } + : undefined, }); taskRunnerFactory!.initialize({ diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index ecec45ade0460..bf7bd709a4a88 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -58,6 +58,7 @@ export interface ActionTypeExecutorOptions { config: Config; secrets: Secrets; params: Params; + proxySettings?: ProxySettings; } export interface ActionResult { @@ -140,3 +141,9 @@ export interface ActionTaskExecutorParams { spaceId: string; actionTaskParamsId: string; } + +export interface ProxySettings { + proxyUrl: string; + proxyHeaders?: Record; + rejectUnauthorizedCertificates: boolean; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/location.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/location.ts index 4a01df3b0ac50..5c0ca74f5225a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/location.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/location.ts @@ -10,7 +10,10 @@ import { getFunctionHelp } from '../../../i18n'; const noop = () => {}; interface Return extends Datatable { - columns: [{ name: 'latitude'; type: 'number' }, { name: 'longitude'; type: 'number' }]; + columns: [ + { id: 'latitude'; name: 'latitude'; meta: { type: 'number' } }, + { id: 'longitude'; name: 'longitude'; meta: { type: 'number' } } + ]; rows: [{ latitude: number; longitude: number }]; } @@ -30,8 +33,8 @@ export function location(): ExpressionFunctionDefinition<'location', null, {}, P return resolve({ type: 'datatable', columns: [ - { name: 'latitude', type: 'number' }, - { name: 'longitude', type: 'number' }, + { id: 'latitude', name: 'latitude', meta: { type: 'number' } }, + { id: 'longitude', name: 'longitude', meta: { type: 'number' } }, ], rows: [{ latitude, longitude }], }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts index b5e5836dc5331..ffb76500a35d6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/__tests__/fixtures/test_tables.ts @@ -16,24 +16,29 @@ const testTable: Datatable = { type: 'datatable', columns: [ { + id: 'name', name: 'name', - type: 'string', + meta: { type: 'string' }, }, { + id: 'time', name: 'time', - type: 'date', + meta: { type: 'date' }, }, { + id: 'price', name: 'price', - type: 'number', + meta: { type: 'number' }, }, { + id: 'quantity', name: 'quantity', - type: 'number', + meta: { type: 'number' }, }, { + id: 'in_stock', name: 'in_stock', - type: 'boolean', + meta: { type: 'boolean' }, }, ], rows: [ @@ -107,24 +112,29 @@ const stringTable: Datatable = { type: 'datatable', columns: [ { + id: 'name', name: 'name', - type: 'string', + meta: { type: 'string' }, }, { + id: 'time', name: 'time', - type: 'string', + meta: { type: 'string' }, }, { + id: 'price', name: 'price', - type: 'string', + meta: { type: 'string' }, }, { + id: 'quantity', name: 'quantity', - type: 'string', + meta: { type: 'string' }, }, { + id: 'in_stock', name: 'in_stock', - type: 'string', + meta: { type: 'string' }, }, ], rows: [ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js index c46b2277859d0..a8c01f0b2791f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.test.js @@ -38,7 +38,7 @@ describe('alterColumn', () => { const arbitraryRowIndex = 6; expect(newColumn.name).not.toBe(originalColumn.name); - expect(newColumn.type).not.toBe(originalColumn.type); + expect(newColumn.meta.type).not.toBe(originalColumn.meta.type); expect(typeof dateToString.rows[arbitraryRowIndex].timeISO).toBe('string'); expect(new Date(dateToString.rows[arbitraryRowIndex].timeISO)).toEqual( new Date(testTable.rows[arbitraryRowIndex].time) @@ -60,7 +60,7 @@ describe('alterColumn', () => { it('converts the column to the specified type', () => { const dateToString = fn(testTable, { column: 'time', type: 'string', name: 'timeISO' }); - expect(typeof dateToString.columns[timeColumnIndex].type).toBe('string'); + expect(typeof dateToString.columns[timeColumnIndex].meta.type).toBe('string'); expect(typeof dateToString.rows[timeColumnIndex].timeISO).toBe('string'); expect(new Date(dateToString.rows[timeColumnIndex].timeISO)).toEqual( new Date(testTable.rows[timeColumnIndex].time) @@ -69,10 +69,10 @@ describe('alterColumn', () => { it('does not change column if type is not specified', () => { const unconvertedColumn = fn(testTable, { column: 'price', name: 'foo' }); - const originalType = testTable.columns[priceColumnIndex].type; + const originalType = testTable.columns[priceColumnIndex].meta.type; const arbitraryRowIndex = 2; - expect(unconvertedColumn.columns[priceColumnIndex].type).toBe(originalType); + expect(unconvertedColumn.columns[priceColumnIndex].meta.type).toBe(originalType); expect(typeof unconvertedColumn.rows[arbitraryRowIndex].foo).toBe(originalType); }); @@ -99,7 +99,7 @@ describe('alterColumn', () => { const arbitraryRowIndex = 5; expect(newColumn.name).not.toBe(originalColumn.name); - expect(newColumn.type).not.toBe(originalColumn.type); + expect(newColumn.meta.type).not.toBe(originalColumn.meta.type); expect(typeof overwriteName.rows[arbitraryRowIndex].name).toBe('string'); expect(new Date(overwriteName.rows[arbitraryRowIndex].name)).toEqual( new Date(testTable.rows[arbitraryRowIndex].time) @@ -122,7 +122,7 @@ describe('alterColumn', () => { const numberToString = fn(testTable, { column: 'price', type: 'string' }); expect(numberToString.columns[priceColumnIndex]).toHaveProperty('name', 'price'); - expect(numberToString.columns[priceColumnIndex]).toHaveProperty('type', 'string'); + expect(numberToString.columns[priceColumnIndex].meta).toHaveProperty('type', 'string'); expect(typeof numberToString.rows[arbitraryRowIndex].price).toBe('string'); expect(numberToString.rows[arbitraryRowIndex].price).toBe( @@ -132,7 +132,7 @@ describe('alterColumn', () => { const stringToNumber = fn(numberToString, { column: 'price', type: 'number' }); expect(stringToNumber.columns[priceColumnIndex]).toHaveProperty('name', 'price'); - expect(stringToNumber.columns[priceColumnIndex]).toHaveProperty('type', 'number'); + expect(stringToNumber.columns[priceColumnIndex].meta).toHaveProperty('type', 'number'); expect(typeof stringToNumber.rows[arbitraryRowIndex].price).toBe('number'); @@ -146,7 +146,7 @@ describe('alterColumn', () => { const dateToString = fn(testTable, { column: 'time', type: 'string' }); expect(dateToString.columns[timeColumnIndex]).toHaveProperty('name', 'time'); - expect(dateToString.columns[timeColumnIndex]).toHaveProperty('type', 'string'); + expect(dateToString.columns[timeColumnIndex].meta).toHaveProperty('type', 'string'); expect(typeof dateToString.rows[arbitraryRowIndex].time).toBe('string'); expect(new Date(dateToString.rows[arbitraryRowIndex].time)).toEqual( @@ -156,7 +156,7 @@ describe('alterColumn', () => { const stringToDate = fn(dateToString, { column: 'time', type: 'date' }); expect(stringToDate.columns[timeColumnIndex]).toHaveProperty('name', 'time'); - expect(stringToDate.columns[timeColumnIndex]).toHaveProperty('type', 'date'); + expect(stringToDate.columns[timeColumnIndex].meta).toHaveProperty('type', 'date'); expect(new Date(stringToDate.rows[timeColumnIndex].time)).toBeInstanceOf(Date); expect(new Date(stringToDate.rows[timeColumnIndex].time)).toEqual( @@ -169,7 +169,7 @@ describe('alterColumn', () => { const arbitraryRowIndex = 1; expect(dateToNumber.columns[timeColumnIndex]).toHaveProperty('name', 'time'); - expect(dateToNumber.columns[timeColumnIndex]).toHaveProperty('type', 'number'); + expect(dateToNumber.columns[timeColumnIndex].meta).toHaveProperty('type', 'number'); expect(typeof dateToNumber.rows[arbitraryRowIndex].time).toBe('number'); expect(dateToNumber.rows[arbitraryRowIndex].time).toEqual( @@ -179,7 +179,7 @@ describe('alterColumn', () => { const numberToDate = fn(dateToNumber, { column: 'time', type: 'date' }); expect(numberToDate.columns[timeColumnIndex]).toHaveProperty('name', 'time'); - expect(numberToDate.columns[timeColumnIndex]).toHaveProperty('type', 'date'); + expect(numberToDate.columns[timeColumnIndex].meta).toHaveProperty('type', 'date'); expect(new Date(numberToDate.rows[arbitraryRowIndex].time)).toBeInstanceOf(Date); expect(new Date(numberToDate.rows[arbitraryRowIndex].time)).toEqual( @@ -192,7 +192,7 @@ describe('alterColumn', () => { const arbitraryRowIndex = 7; expect(booleanToNumber.columns[inStockColumnIndex]).toHaveProperty('name', 'in_stock'); - expect(booleanToNumber.columns[inStockColumnIndex]).toHaveProperty('type', 'number'); + expect(booleanToNumber.columns[inStockColumnIndex].meta).toHaveProperty('type', 'number'); expect(typeof booleanToNumber.rows[arbitraryRowIndex].in_stock).toBe('number'); expect(booleanToNumber.rows[arbitraryRowIndex].in_stock).toEqual( @@ -202,7 +202,7 @@ describe('alterColumn', () => { const numberToBoolean = fn(booleanToNumber, { column: 'in_stock', type: 'boolean' }); expect(numberToBoolean.columns[inStockColumnIndex]).toHaveProperty('name', 'in_stock'); - expect(numberToBoolean.columns[inStockColumnIndex]).toHaveProperty('type', 'boolean'); + expect(numberToBoolean.columns[inStockColumnIndex].meta).toHaveProperty('type', 'boolean'); expect(typeof numberToBoolean.rows[arbitraryRowIndex].in_stock).toBe('boolean'); expect(numberToBoolean.rows[arbitraryRowIndex].in_stock).toEqual( @@ -216,7 +216,7 @@ describe('alterColumn', () => { expect(stringToNull.columns[nameColumnIndex]).toHaveProperty('name', 'name'); - expect(stringToNull.columns[nameColumnIndex]).toHaveProperty('type', 'null'); + expect(stringToNull.columns[nameColumnIndex].meta).toHaveProperty('type', 'null'); expect(stringToNull.rows[arbitraryRowIndex].name).toBe(null); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts index 68c1957c808a3..531959f6bc63a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/alterColumn.ts @@ -57,14 +57,14 @@ export function alterColumn(): ExpressionFunctionDefinition< } const name = args.name || column.name; - const type = args.type || column.type; + const type = args.type || column.meta.type; const columns = input.columns.reduce((all: DatatableColumn[], col) => { if (col.name !== args.name) { if (col.name !== column.name) { all.push(col); } else { - all.push({ name, type }); + all.push({ id: name, name, meta: { type } }); } } return all; @@ -76,7 +76,7 @@ export function alterColumn(): ExpressionFunctionDefinition< handler = (function getHandler() { switch (type) { case 'string': - if (column.type === 'date') { + if (column.meta.type === 'date') { return (v: string) => new Date(v).toISOString(); } return String; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.test.js index 2fc9491f6b5b7..49d14622e80f0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.test.js @@ -13,19 +13,19 @@ describe('as', () => { it('returns a datatable with a single column and single row', () => { expect(fn('foo', { name: 'bar' })).toEqual({ type: 'datatable', - columns: [{ name: 'bar', type: 'string' }], + columns: [{ id: 'bar', name: 'bar', meta: { type: 'string' } }], rows: [{ bar: 'foo' }], }); expect(fn(2, { name: 'num' })).toEqual({ type: 'datatable', - columns: [{ name: 'num', type: 'number' }], + columns: [{ id: 'num', name: 'num', meta: { type: 'number' } }], rows: [{ num: 2 }], }); expect(fn(true, { name: 'bool' })).toEqual({ type: 'datatable', - columns: [{ name: 'bool', type: 'boolean' }], + columns: [{ id: 'bool', name: 'bool', meta: { type: 'boolean' } }], rows: [{ bool: true }], }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.ts index 9c10e85227398..d8fd948c12b2e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/as.ts @@ -34,8 +34,9 @@ export function asFn(): ExpressionFunctionDefinition<'as', Input, Arguments, Dat type: 'datatable', columns: [ { + id: args.name, name: args.name, - type: getType(input), + meta: { type: getType(input) }, }, ], rows: [ diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js index e5ef06d1503ee..652d61fd77398 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.test.js @@ -14,13 +14,17 @@ describe('mapColumn', () => { const fn = functionWrapper(mapColumn); it('returns a datatable with a new column with the values from mapping a function over each row in a datatable', () => { - return fn(testTable, { name: 'pricePlusTwo', expression: pricePlusTwo }).then((result) => { + return fn(testTable, { + id: 'pricePlusTwo', + name: 'pricePlusTwo', + expression: pricePlusTwo, + }).then((result) => { const arbitraryRowIndex = 2; expect(result.type).toBe('datatable'); expect(result.columns).toEqual([ ...testTable.columns, - { name: 'pricePlusTwo', type: 'number' }, + { id: 'pricePlusTwo', name: 'pricePlusTwo', meta: { type: 'number' } }, ]); expect(result.columns[result.columns.length - 1]).toHaveProperty('name', 'pricePlusTwo'); expect(result.rows[arbitraryRowIndex]).toHaveProperty('pricePlusTwo'); @@ -35,7 +39,7 @@ describe('mapColumn', () => { expect(result.type).toBe('datatable'); expect(result.columns).toHaveLength(testTable.columns.length); expect(result.columns[nameColumnIndex]).toHaveProperty('name', 'name'); - expect(result.columns[nameColumnIndex]).toHaveProperty('type', 'number'); + expect(result.columns[nameColumnIndex].meta).toHaveProperty('type', 'number'); expect(result.rows[arbitraryRowIndex]).toHaveProperty('name', 202); }); }); @@ -45,7 +49,7 @@ describe('mapColumn', () => { expect(result.type).toBe('datatable'); expect(result.columns).toHaveLength(1); expect(result.columns[0]).toHaveProperty('name', 'name'); - expect(result.columns[0]).toHaveProperty('type', 'null'); + expect(result.columns[0].meta).toHaveProperty('type', 'null'); }); }); @@ -56,7 +60,7 @@ describe('mapColumn', () => { const arbitraryRowIndex = 8; expect(result.columns[emptyColumnIndex]).toHaveProperty('name', 'empty'); - expect(result.columns[emptyColumnIndex]).toHaveProperty('type', 'null'); + expect(result.columns[emptyColumnIndex].meta).toHaveProperty('type', 'null'); expect(result.rows[arbitraryRowIndex]).toHaveProperty('empty', null); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts index 7dd309cba5c64..6d6a432e5553e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/mapColumn.ts @@ -59,7 +59,7 @@ export function mapColumn(): ExpressionFunctionDefinition< return Promise.all(rowPromises).then((rows) => { const existingColumnIndex = columns.findIndex(({ name }) => name === args.name); const type = rows.length ? getType(rows[0][args.name]) : 'null'; - const newColumn = { name: args.name, type }; + const newColumn = { id: args.name, name: args.name, meta: { type } }; if (existingColumnIndex === -1) { columns.push(newColumn); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js index 2dfb9eeea76bc..07d436007c816 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/ply.test.js @@ -16,7 +16,7 @@ const averagePrice = (datatable) => { return Promise.resolve({ type: 'datatable', - columns: [{ name: 'average_price', type: 'number' }], + columns: [{ id: 'average_price', name: 'average_price', meta: { type: 'number' } }], rows: [{ average_price: average }], }); }; @@ -26,7 +26,7 @@ const doublePrice = (datatable) => { return Promise.resolve({ type: 'datatable', - columns: [{ name: 'double_price', type: 'number' }], + columns: [{ id: 'double_price', name: 'double_price', meta: { type: 'number' } }], rows: newRows, }); }; @@ -34,7 +34,7 @@ const doublePrice = (datatable) => { const rowCount = (datatable) => { return Promise.resolve({ type: 'datatable', - columns: [{ name: 'row_count', type: 'number' }], + columns: [{ id: 'row_count', name: 'row_count', meta: { type: 'number' } }], rows: [ { row_count: datatable.rows.length, @@ -53,10 +53,10 @@ describe('ply', () => { (result) => { expect(result.type).toBe('datatable'); expect(result.columns).toEqual([ - { name: 'name', type: 'string' }, - { name: 'in_stock', type: 'boolean' }, - { name: 'average_price', type: 'number' }, - { name: 'row_count', type: 'number' }, + { id: 'name', name: 'name', meta: { type: 'string' } }, + { id: 'in_stock', name: 'in_stock', meta: { type: 'boolean' } }, + { id: 'average_price', name: 'average_price', meta: { type: 'number' } }, + { id: 'row_count', name: 'row_count', meta: { type: 'number' } }, ]); expect(result.rows[arbitaryRowIndex]).toHaveProperty('average_price'); expect(result.rows[arbitaryRowIndex]).toHaveProperty('row_count'); @@ -75,7 +75,7 @@ describe('ply', () => { expect(result).toEqual({ type: 'datatable', rows: [{ row_count: testTable.rows.length }], - columns: [{ name: 'row_count', type: 'number' }], + columns: [{ id: 'row_count', name: 'row_count', meta: { type: 'number' } }], }) ); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.test.js index ff11669db05f7..d137ce05ccc19 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.test.js @@ -15,7 +15,10 @@ describe('staticColumn', () => { const result = fn(testTable, { name: 'foo', value: 'bar' }); expect(result.type).toBe('datatable'); - expect(result.columns).toEqual([...testTable.columns, { name: 'foo', type: 'string' }]); + expect(result.columns).toEqual([ + ...testTable.columns, + { id: 'foo', name: 'foo', meta: { type: 'string' } }, + ]); expect(result.rows.every((row) => typeof row.foo === 'string')).toBe(true); expect(result.rows.every((row) => row.foo === 'bar')).toBe(true); }); @@ -33,7 +36,10 @@ describe('staticColumn', () => { const result = fn(testTable, { name: 'empty' }); expect(result.type).toBe('datatable'); - expect(result.columns).toEqual([...testTable.columns, { name: 'empty', type: 'null' }]); + expect(result.columns).toEqual([ + ...testTable.columns, + { id: 'empty', name: 'empty', meta: { type: 'null' } }, + ]); expect(result.rows.every((row) => row.empty === null)).toBe(true); }); @@ -41,7 +47,7 @@ describe('staticColumn', () => { const result = fn(emptyTable, { name: 'empty', value: 1 }); expect(result.type).toBe('datatable'); - expect(result.columns).toEqual([{ name: 'empty', type: 'number' }]); + expect(result.columns).toEqual([{ id: 'empty', name: 'empty', meta: { type: 'number' } }]); expect(result.rows.length).toBe(0); }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts index 4fa4be0a2f09f..63a115c7e630b 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/staticColumn.ts @@ -48,7 +48,7 @@ export function staticColumn(): ExpressionFunctionDefinition< const type = getType(args.value) as DatatableColumnType; const columns = [...input.columns]; const existingColumnIndex = columns.findIndex(({ name }) => name === args.name); - const newColumn = { name: args.name, type }; + const newColumn = { id: args.name, name: args.name, meta: { type } }; if (existingColumnIndex > -1) { columns[existingColumnIndex] = newColumn; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts index 60d5edeb10483..e29f1f511685e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/demodata/index.ts @@ -50,26 +50,26 @@ export function demodata(): ExpressionFunctionDefinition< if (args.type === DemoRows.CI) { set = { columns: [ - { name: '@timestamp', type: 'date' }, - { name: 'time', type: 'date' }, - { name: 'cost', type: 'number' }, - { name: 'username', type: 'string' }, - { name: 'price', type: 'number' }, - { name: 'age', type: 'number' }, - { name: 'country', type: 'string' }, - { name: 'state', type: 'string' }, - { name: 'project', type: 'string' }, - { name: 'percent_uptime', type: 'number' }, + { id: '@timestamp', name: '@timestamp', meta: { type: 'date' } }, + { id: 'time', name: 'time', meta: { type: 'date' } }, + { id: 'cost', name: 'cost', meta: { type: 'number' } }, + { id: 'username', name: 'username', meta: { type: 'string' } }, + { id: 'price', name: 'price', meta: { type: 'number' } }, + { id: 'age', name: 'age', meta: { type: 'number' } }, + { id: 'country', name: 'country', meta: { type: 'string' } }, + { id: 'state', name: 'state', meta: { type: 'string' } }, + { id: 'project', name: 'project', meta: { type: 'string' } }, + { id: 'percent_uptime', name: 'percent_uptime', meta: { type: 'number' } }, ], rows: sortBy(demoRows, 'time'), }; } else if (args.type === DemoRows.SHIRTS) { set = { columns: [ - { name: 'size', type: 'string' }, - { name: 'color', type: 'string' }, - { name: 'price', type: 'number' }, - { name: 'cut', type: 'string' }, + { id: 'size', name: 'size', meta: { type: 'string' } }, + { id: 'color', name: 'color', meta: { type: 'string' } }, + { id: 'price', name: 'price', meta: { type: 'number' } }, + { id: 'cut', name: 'cut', meta: { type: 'string' } }, ], rows: demoRows, }; diff --git a/x-pack/plugins/canvas/common/lib/get_field_type.ts b/x-pack/plugins/canvas/common/lib/get_field_type.ts index db817393a1cdb..d081f8b69956a 100644 --- a/x-pack/plugins/canvas/common/lib/get_field_type.ts +++ b/x-pack/plugins/canvas/common/lib/get_field_type.ts @@ -20,5 +20,5 @@ export function getFieldType(columns: DatatableColumn[], field?: string): string } const realField = unquoteString(field); const column = columns.find((dataTableColumn) => dataTableColumn.name === realField); - return column ? column.type : 'null'; + return column ? column.meta.type : 'null'; } diff --git a/x-pack/plugins/canvas/public/components/datatable/datatable.tsx b/x-pack/plugins/canvas/public/components/datatable/datatable.tsx index 4f06ac0749aaf..bd343b15758bf 100644 --- a/x-pack/plugins/canvas/public/components/datatable/datatable.tsx +++ b/x-pack/plugins/canvas/public/components/datatable/datatable.tsx @@ -41,7 +41,7 @@ const getIcon = (type: IconType) => { const getColumnName = (col: DatatableColumn) => (typeof col === 'string' ? col : col.name); -const getColumnType = (col: DatatableColumn) => col.type || null; +const getColumnType = (col: DatatableColumn) => col.meta?.type || null; const getFormattedValue = (val: any, type: any) => { if (type === 'date') { diff --git a/x-pack/plugins/canvas/public/functions/timelion.ts b/x-pack/plugins/canvas/public/functions/timelion.ts index 4eb34e838d18a..947972fa310c9 100644 --- a/x-pack/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/plugins/canvas/public/functions/timelion.ts @@ -135,10 +135,13 @@ export function timelionFunctionFactory(initialize: InitializeArguments): () => return { type: 'datatable', + meta: { + source: 'timelion', + }, columns: [ - { name: '@timestamp', type: 'date' }, - { name: 'value', type: 'number' }, - { name: 'label', type: 'string' }, + { id: '@timestamp', name: '@timestamp', meta: { type: 'date' } }, + { id: 'value', name: 'value', meta: { type: 'number' } }, + { id: 'label', name: 'label', meta: { type: 'string' } }, ], rows, }; diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts index c3c122d1e301a..c8bdf01ad7991 100644 --- a/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.test.ts @@ -53,7 +53,11 @@ describe('query_es_sql', () => { const result = await queryEsSQL(api, baseArgs); - const expectedColumns = response.columns.map((c) => ({ name: c.name, type: 'string' })); + const expectedColumns = response.columns.map((c) => ({ + id: c.name, + name: c.name, + meta: { type: 'string' }, + })); const columnNames = expectedColumns.map((c) => c.name); const expectedRows = response.rows.map((r) => zipObject(columnNames, r)); diff --git a/x-pack/plugins/canvas/server/lib/query_es_sql.ts b/x-pack/plugins/canvas/server/lib/query_es_sql.ts index 8639cfa31dca8..941dc244330e8 100644 --- a/x-pack/plugins/canvas/server/lib/query_es_sql.ts +++ b/x-pack/plugins/canvas/server/lib/query_es_sql.ts @@ -53,7 +53,11 @@ export const queryEsSQL = async ( }); const columns = response.columns.map(({ name, type }) => { - return { name: sanitizeName(name), type: normalizeType(type) }; + return { + id: sanitizeName(name), + name: sanitizeName(name), + meta: { type: normalizeType(type) }, + }; }); const columnNames = map(columns, 'name'); let rows = response.rows.map((row) => zipObject(columnNames, row)); @@ -82,6 +86,9 @@ export const queryEsSQL = async ( return { type: 'datatable', + meta: { + type: 'essql', + }, columns, rows, }; diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts new file mode 100644 index 0000000000000..d0c597532f6ed --- /dev/null +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/drilldown_shared.ts @@ -0,0 +1,32 @@ +/* + * 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 { + APPLY_FILTER_TRIGGER, + SELECT_RANGE_TRIGGER, + TriggerId, + VALUE_CLICK_TRIGGER, +} from '../../../../../../../src/plugins/ui_actions/public'; + +/** + * We know that VALUE_CLICK_TRIGGER and SELECT_RANGE_TRIGGER are also triggering APPLY_FILTER_TRIGGER + * This function appends APPLY_FILTER_TRIGGER to list of triggers if VALUE_CLICK_TRIGGER or SELECT_RANGE_TRIGGER + * + * TODO: this probably should be part of uiActions infrastructure, + * but dynamic implementation of nested trigger doesn't allow to statically express such relations + * + * @param triggers + */ +export function ensureNestedTriggers(triggers: TriggerId[]): TriggerId[] { + if ( + !triggers.includes(APPLY_FILTER_TRIGGER) && + (triggers.includes(VALUE_CLICK_TRIGGER) || triggers.includes(SELECT_RANGE_TRIGGER)) + ) { + return [...triggers, APPLY_FILTER_TRIGGER]; + } + + return triggers; +} diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx index 712a46dc32e08..d5547ff8097cc 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.test.tsx @@ -10,9 +10,13 @@ import { } from './flyout_create_drilldown'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { ViewMode } from '../../../../../../../../src/plugins/embeddable/public'; -import { TriggerContextMapping } from '../../../../../../../../src/plugins/ui_actions/public'; +import { + TriggerContextMapping, + TriggerId, +} from '../../../../../../../../src/plugins/ui_actions/public'; import { MockEmbeddable, enhanceEmbeddable } from '../test_helpers'; import { uiActionsEnhancedPluginMock } from '../../../../../../ui_actions_enhanced/public/mocks'; +import { UiActionsEnhancedActionFactory } from '../../../../../../ui_actions_enhanced/public/'; const overlays = coreMock.createStart().overlays; const uiActionsEnhanced = uiActionsEnhancedPluginMock.createStartContract(); @@ -50,6 +54,7 @@ interface CompatibilityParams { isValueClickTriggerSupported?: boolean; isEmbeddableEnhanced?: boolean; rootType?: string; + actionFactoriesTriggers?: TriggerId[]; } describe('isCompatible', () => { @@ -61,9 +66,16 @@ describe('isCompatible', () => { isValueClickTriggerSupported = true, isEmbeddableEnhanced = true, rootType = 'dashboard', + actionFactoriesTriggers = ['VALUE_CLICK_TRIGGER'], }: CompatibilityParams, expectedResult: boolean = true ): Promise { + uiActionsEnhanced.getActionFactories.mockImplementation(() => [ + ({ + supportedTriggers: () => actionFactoriesTriggers, + } as unknown) as UiActionsEnhancedActionFactory, + ]); + let embeddable = new MockEmbeddable( { id: '', viewMode: isEdit ? ViewMode.EDIT : ViewMode.VIEW }, { @@ -116,6 +128,15 @@ describe('isCompatible', () => { rootType: 'visualization', }); }); + + test('not compatible if no triggers intersection', async () => { + await assertNonCompatibility({ + actionFactoriesTriggers: [], + }); + await assertNonCompatibility({ + actionFactoriesTriggers: ['SELECT_RANGE_TRIGGER'], + }); + }); }); describe('execute', () => { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index 2de862a6708a8..b5e5e248eaeb1 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -6,17 +6,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - ActionByType, - APPLY_FILTER_TRIGGER, - SELECT_RANGE_TRIGGER, - VALUE_CLICK_TRIGGER, -} from '../../../../../../../../src/plugins/ui_actions/public'; +import { ActionByType } from '../../../../../../../../src/plugins/ui_actions/public'; import { toMountPoint } from '../../../../../../../../src/plugins/kibana_react/public'; import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; import { EmbeddableContext } from '../../../../../../../../src/plugins/embeddable/public'; import { StartDependencies } from '../../../../plugin'; import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { ensureNestedTriggers } from '../drilldown_shared'; export const OPEN_FLYOUT_ADD_DRILLDOWN = 'OPEN_FLYOUT_ADD_DRILLDOWN'; @@ -47,8 +43,18 @@ export class FlyoutCreateDrilldownAction implements ActionByType - [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, APPLY_FILTER_TRIGGER].includes(trigger) + /** + * Check if there is an intersection between all registered drilldowns possible triggers that they could be attached to + * and triggers that current embeddable supports + */ + const allPossibleTriggers = this.params + .start() + .plugins.uiActionsEnhanced.getActionFactories() + .map((factory) => factory.supportedTriggers()) + .reduce((res, next) => res.concat(next), []); + + return ensureNestedTriggers(supportedTriggers).some((trigger) => + allPossibleTriggers.includes(trigger) ); } @@ -73,6 +79,7 @@ export class FlyoutCreateDrilldownAction implements ActionByType handle.close()} viewMode={'create'} dynamicActionManager={embeddable.enhancements.dynamicActions} + supportedTriggers={ensureNestedTriggers(embeddable.supportedTriggers())} /> ), { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx index b9ae45c2853c3..2950f5bf7110e 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.test.tsx @@ -22,6 +22,9 @@ uiActionsPlugin.setup.registerDrilldown({ isConfigValid: () => true, execute: async () => {}, getDisplayName: () => 'test', + supportedTriggers() { + return ['VALUE_CLICK_TRIGGER']; + }, }); const actionParams: FlyoutEditDrilldownParams = { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index af1ae67454463..6dfda93db7155 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -16,6 +16,7 @@ import { MenuItem } from './menu_item'; import { isEnhancedEmbeddable } from '../../../../../../embeddable_enhanced/public'; import { StartDependencies } from '../../../../plugin'; import { StartServicesGetter } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { ensureNestedTriggers } from '../drilldown_shared'; export const OPEN_FLYOUT_EDIT_DRILLDOWN = 'OPEN_FLYOUT_EDIT_DRILLDOWN'; @@ -62,6 +63,7 @@ export class FlyoutEditDrilldownAction implements ActionByType handle.close()} viewMode={'manage'} dynamicActionManager={embeddable.enhancements.dynamicActions} + supportedTriggers={ensureNestedTriggers(embeddable.supportedTriggers())} /> ), { diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx index 6d6803510a281..5cbf65f7645dd 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/components/collect_config_container.tsx @@ -11,7 +11,7 @@ import { SimpleSavedObject } from '../../../../../../../../src/core/public'; import { DashboardDrilldownConfig } from './dashboard_drilldown_config'; import { txtDestinationDashboardNotFound } from './i18n'; import { CollectConfigProps } from '../../../../../../../../src/plugins/kibana_utils/public'; -import { Config } from '../types'; +import { Config, FactoryContext } from '../types'; import { Params } from '../drilldown'; const mergeDashboards = ( @@ -34,7 +34,7 @@ const dashboardSavedObjectToMenuItem = ( label: savedObject.attributes.title, }); -interface DashboardDrilldownCollectConfigProps extends CollectConfigProps { +interface DashboardDrilldownCollectConfigProps extends CollectConfigProps { params: Params; } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index 703acbc8d9d59..a17d95c37c5ce 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { reactToUiComponent } from '../../../../../../../src/plugins/kibana_react/public'; +import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public'; import { DashboardUrlGenerator, DashboardUrlGeneratorState, @@ -23,7 +24,7 @@ import { } from '../../../../../../../src/plugins/data/public'; import { StartServicesGetter } from '../../../../../../../src/plugins/kibana_utils/public'; import { StartDependencies } from '../../../plugin'; -import { Config } from './types'; +import { Config, FactoryContext } from './types'; export interface Params { start: StartServicesGetter>; @@ -31,7 +32,7 @@ export interface Params { } export class DashboardToDashboardDrilldown - implements Drilldown { + implements Drilldown { constructor(protected readonly params: Params) {} public readonly id = DASHBOARD_TO_DASHBOARD_DRILLDOWN; @@ -59,6 +60,10 @@ export class DashboardToDashboardDrilldown return true; }; + public supportedTriggers(): Array { + return [APPLY_FILTER_TRIGGER]; + } + public readonly getHref = async ( config: Config, context: ApplyGlobalFilterActionContext diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts index 426e250499de0..c21109f8a596a 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/types.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { UiActionsEnhancedBaseActionFactoryContext } from '../../../../../ui_actions_enhanced/public'; +import { APPLY_FILTER_TRIGGER } from '../../../../../../../src/plugins/ui_actions/public'; + export interface Config { dashboardId?: string; useCurrentFilters: boolean; useCurrentDateRange: boolean; } + +export type FactoryContext = UiActionsEnhancedBaseActionFactoryContext; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js b/x-pack/plugins/enterprise_search/common/version.ts similarity index 54% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js rename to x-pack/plugins/enterprise_search/common/version.ts index 5f909ab2c0f79..e29ad8a9f866b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/index.js +++ b/x-pack/plugins/enterprise_search/common/version.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { DeletePhase } from './delete_phase.container'; +import { SemVer } from 'semver'; +import pkg from '../../../../package.json'; + +export const CURRENT_VERSION = new SemVer(pkg.version as string); +export const CURRENT_MAJOR_VERSION = CURRENT_VERSION.major; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts index b1d7341d51a4c..ef3bf54053b5c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts @@ -5,6 +5,7 @@ */ import { httpServiceMock } from 'src/core/public/mocks'; +import { ExternalUrl } from '../shared/enterprise_search_url'; /** * A set of default Kibana context values to use across component tests. @@ -14,5 +15,6 @@ export const mockKibanaContext = { http: httpServiceMock.createSetupContract(), setBreadcrumbs: jest.fn(), setDocTitle: jest.fn(), - enterpriseSearchUrl: 'http://localhost:3002', + config: { host: 'http://localhost:3002' }, + externalUrl: new ExternalUrl('http://localhost:3002'), }; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx index 1e0df1326c177..9f8fda856eed6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx @@ -21,7 +21,7 @@ import { mockLicenseContext } from './license_context.mock'; * * Example usage: * - * const wrapper = mountWithContext(, { enterpriseSearchUrl: 'someOverride', license: {} }); + * const wrapper = mountWithContext(, { config: { host: 'someOverride' } }); */ export const mountWithContext = (children: React.ReactNode, context?: object) => { return mount( diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts index 2bcdd42c38055..792be49a49c48 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts @@ -35,6 +35,6 @@ jest.mock('react', () => ({ * // ... etc. * * it('some test', () => { - * useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: 'someOverride' })); + * useContext.mockImplementationOnce(() => ({ config: { host: 'someOverride' } })); * }); */ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx index 4d2b790e7fb97..9b0edb423bc52 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx @@ -11,16 +11,20 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../shared/telemetry'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { KibanaContext, IKibanaContext } from '../../../index'; +import { CREATE_ENGINES_PATH } from '../../routes'; import { EngineOverviewHeader } from '../engine_overview_header'; import './empty_states.scss'; export const EmptyState: React.FC = () => { - const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; + const { + externalUrl: { getAppSearchUrl }, + http, + } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { - href: `${enterpriseSearchUrl}/as/engines/new`, + href: getAppSearchUrl(CREATE_ENGINES_PATH), target: '_blank', onClick: () => sendTelemetry({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx index 1e58d820dc83b..9c6122c88c7d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; +import { getEngineRoute } from '../../routes'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; @@ -39,9 +40,13 @@ export const EngineTable: React.FC = ({ data, pagination: { totalEngines, pageIndex, onPaginate }, }) => { - const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; + const { + externalUrl: { getAppSearchUrl }, + http, + } = useContext(KibanaContext) as IKibanaContext; + const engineLinkProps = (name: string) => ({ - href: `${enterpriseSearchUrl}/as/engines/${name}`, + href: getAppSearchUrl(getEngineRoute(name)), target: '_blank', onClick: () => sendTelemetry({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx index cc480d241ad50..7f67d00f5df91 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx @@ -19,13 +19,16 @@ import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; export const EngineOverviewHeader: React.FC = () => { - const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; + const { + externalUrl: { getAppSearchUrl }, + http, + } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { fill: true, iconType: 'popout', 'data-test-subj': 'launchButton', - href: `${enterpriseSearchUrl}/as`, + href: getAppSearchUrl(), target: '_blank', onClick: () => sendTelemetry({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 9e660d10053ec..fa9a761a966e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -20,8 +20,8 @@ describe('AppSearch', () => { expect(wrapper.find(Layout)).toHaveLength(1); }); - it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ enterpriseSearchUrl: '' })); + it('redirects to Setup Guide when config.host is not set', () => { + (useContext as jest.Mock).mockImplementationOnce(() => ({ config: { host: '' } })); const wrapper = shallow(); expect(wrapper.find(Redirect)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index d69b3ba29b0ca..7ebd35ff35ee1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -13,19 +13,29 @@ import { APP_SEARCH_PLUGIN } from '../../../common/constants'; import { KibanaContext, IKibanaContext } from '../index'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; +import { + ROOT_PATH, + SETUP_GUIDE_PATH, + SETTINGS_PATH, + CREDENTIALS_PATH, + ROLE_MAPPINGS_PATH, + ENGINES_PATH, +} from './routes'; + import { SetupGuide } from './components/setup_guide'; import { EngineOverview } from './components/engine_overview'; export const AppSearch: React.FC = () => { - const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; - if (!enterpriseSearchUrl) + const { config } = useContext(KibanaContext) as IKibanaContext; + + if (!config.host) return ( - + - + {/* Kibana displays a blank page on redirect if this isn't included */} @@ -33,17 +43,17 @@ export const AppSearch: React.FC = () => { return ( - + }> - + {/* For some reason a Redirect to /engines just doesn't work here - it shows a blank page */} - + @@ -54,27 +64,28 @@ export const AppSearch: React.FC = () => { }; export const AppSearchNav: React.FC = () => { - const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; - const externalUrl = `${enterpriseSearchUrl}/as#`; + const { + externalUrl: { getAppSearchUrl }, + } = useContext(KibanaContext) as IKibanaContext; return ( - + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.engines', { defaultMessage: 'Engines', })} - + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.settings', { defaultMessage: 'Account Settings', })} - + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.credentials', { defaultMessage: 'Credentials', })} - + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.roleMappings', { defaultMessage: 'Role Mappings', })} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts new file mode 100644 index 0000000000000..51e2497365dd7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.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. + */ + +export const ROOT_PATH = '/'; +export const SETUP_GUIDE_PATH = '/setup_guide'; +export const SETTINGS_PATH = '/settings/account'; +export const CREDENTIALS_PATH = '/credentials'; +export const ROLE_MAPPINGS_PATH = '#/role-mappings'; // This page seems to 404 if the # isn't included + +export const ENGINES_PATH = '/engines'; +export const CREATE_ENGINES_PATH = `${ENGINES_PATH}/new`; + +export const ENGINE_PATH = '/engines/:engineName'; +export const getEngineRoute = (engineName: string) => `${ENGINES_PATH}/${engineName}`; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 70e16e61846b4..e0cf2814b46b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -17,10 +17,11 @@ import { WorkplaceSearch } from './workplace_search'; describe('renderApp', () => { let params: AppMountParameters; const core = coreMock.createStart(); - const config = {}; const plugins = { licensing: licensingMock.createSetup(), } as any; + const config = {}; + const data = {} as any; beforeEach(() => { jest.clearAllMocks(); @@ -30,19 +31,19 @@ describe('renderApp', () => { it('mounts and unmounts UI', () => { const MockApp = () =>
Hello world!
; - const unmount = renderApp(MockApp, core, params, config, plugins); + const unmount = renderApp(MockApp, params, core, plugins, config, data); expect(params.element.querySelector('.hello-world')).not.toBeNull(); unmount(); expect(params.element.innerHTML).toEqual(''); }); it('renders AppSearch', () => { - renderApp(AppSearch, core, params, config, plugins); + renderApp(AppSearch, params, core, plugins, config, data); expect(params.element.querySelector('.setupGuide')).not.toBeNull(); }); it('renders WorkplaceSearch', () => { - renderApp(WorkplaceSearch, core, params, config, plugins); + renderApp(WorkplaceSearch, params, core, plugins, config, data); expect(params.element.querySelector('.setupGuide')).not.toBeNull(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 0e43d86f5095d..f3ccbc126ae62 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -10,11 +10,13 @@ import { Router } from 'react-router-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreStart, AppMountParameters, HttpSetup, ChromeBreadcrumb } from 'src/core/public'; -import { ClientConfigType, PluginsSetup } from '../plugin'; +import { ClientConfigType, ClientData, PluginsSetup } from '../plugin'; import { LicenseProvider } from './shared/licensing'; +import { IExternalUrl } from './shared/enterprise_search_url'; export interface IKibanaContext { - enterpriseSearchUrl?: string; + config: { host?: string }; + externalUrl: IExternalUrl; http: HttpSetup; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; setDocTitle(title: string): void; @@ -30,17 +32,19 @@ export const KibanaContext = React.createContext({}); export const renderApp = ( App: React.FC, - core: CoreStart, params: AppMountParameters, + core: CoreStart, + plugins: PluginsSetup, config: ClientConfigType, - plugins: PluginsSetup + data: ClientData ) => { ReactDOM.render( { + const externalUrl = new ExternalUrl('http://localhost:3002'); + + it('exposes a public enterpriseSearchUrl string', () => { + expect(externalUrl.enterpriseSearchUrl).toEqual('http://localhost:3002'); + }); + + it('generates a public App Search URL', () => { + expect(externalUrl.getAppSearchUrl()).toEqual('http://localhost:3002/as'); + expect(externalUrl.getAppSearchUrl('/path')).toEqual('http://localhost:3002/as/path'); + }); + + it('generates a public Workplace Search URL', () => { + expect(externalUrl.getWorkplaceSearchUrl()).toEqual('http://localhost:3002/ws'); + expect(externalUrl.getWorkplaceSearchUrl('/path')).toEqual('http://localhost:3002/ws/path'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/generate_external_url.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/generate_external_url.ts new file mode 100644 index 0000000000000..9db48d197f3bc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/generate_external_url.ts @@ -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. + */ + +/** + * Small helper for generating external public-facing URLs + * to the legacy/standalone Enterprise Search app + */ +export interface IExternalUrl { + enterpriseSearchUrl?: string; + getAppSearchUrl(path?: string): string; + getWorkplaceSearchUrl(path?: string): string; +} + +export class ExternalUrl { + public enterpriseSearchUrl: string; + + constructor(externalUrl: string) { + this.enterpriseSearchUrl = externalUrl; + + this.getAppSearchUrl = this.getAppSearchUrl.bind(this); + this.getWorkplaceSearchUrl = this.getWorkplaceSearchUrl.bind(this); + } + + private getExternalUrl(path: string): string { + return this.enterpriseSearchUrl + path; + } + + public getAppSearchUrl(path: string = ''): string { + return this.getExternalUrl('/as' + path); + } + + public getWorkplaceSearchUrl(path: string = ''): string { + return this.getExternalUrl('/ws' + path); + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts index bbbb688b8ea7b..563d19f9fdeb5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts @@ -5,3 +5,4 @@ */ export { getPublicUrl } from './get_enterprise_search_url'; +export { ExternalUrl, IExternalUrl } from './generate_external_url'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index ccd5beff66e70..a2cb424dadee8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -14,7 +14,7 @@ import { KibanaContext, IKibanaContext } from '../../index'; import './error_state_prompt.scss'; export const ErrorStatePrompt: React.FC = () => { - const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; + const { config } = useContext(KibanaContext) as IKibanaContext; return ( { id="xpack.enterpriseSearch.errorConnectingState.description1" defaultMessage="We can’t establish a connection to Enterprise Search at the host URL: {enterpriseSearchUrl}" values={{ - enterpriseSearchUrl: {enterpriseSearchUrl}, + enterpriseSearchUrl: {config.host}, }} />

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx index e1114986d2244..53f3a7a274429 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/error_state/error_state.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +// TODO: Remove EuiPage & EuiPageBody before exposing full app + import React from 'react'; import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/use_routes/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts similarity index 84% rename from x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/use_routes/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts index cb9684408c459..41861a8ee2dc5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/use_routes/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { useRoutes } from './use_routes'; +export { WorkplaceSearchNav } from './nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx new file mode 100644 index 0000000000000..0e85d8467cff0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -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 '../../../__mocks__/shallow_usecontext.mock'; +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SideNav, SideNavLink } from '../../../shared/layout'; +import { WorkplaceSearchNav } from './'; + +describe('WorkplaceSearchNav', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SideNav)).toHaveLength(1); + expect(wrapper.find(SideNavLink).first().prop('to')).toEqual('/'); + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('http://localhost:3002/ws/search'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx new file mode 100644 index 0000000000000..9fb627ed09791 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -0,0 +1,75 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiSpacer } from '@elastic/eui'; + +import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { KibanaContext, IKibanaContext } from '../../../index'; +import { SideNav, SideNavLink } from '../../../shared/layout'; + +import { + ORG_SOURCES_PATH, + SOURCES_PATH, + SECURITY_PATH, + ROLE_MAPPINGS_PATH, + GROUPS_PATH, + ORG_SETTINGS_PATH, +} from '../../routes'; + +export const WorkplaceSearchNav: React.FC = () => { + const { + externalUrl: { getWorkplaceSearchUrl }, + } = useContext(KibanaContext) as IKibanaContext; + + // TODO: icons + return ( + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.overview', { + defaultMessage: 'Overview', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.sources', { + defaultMessage: 'Sources', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.groups', { + defaultMessage: 'Groups', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.roleMappings', { + defaultMessage: 'Role Mappings', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.security', { + defaultMessage: 'Security', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.settings', { + defaultMessage: 'Settings', + })} + + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.personalDashboard', { + defaultMessage: 'View my personal dashboard', + })} + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.search', { + defaultMessage: 'Go to search application', + })} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.tsx index 288c0be84fa9a..786357358dfa6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_card.tsx @@ -17,7 +17,6 @@ import { EuiButtonEmptyProps, EuiLinkProps, } from '@elastic/eui'; -import { useRoutes } from '../shared/use_routes'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; @@ -40,8 +39,10 @@ export const OnboardingCard: React.FC = ({ actionPath, complete, }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; - const { getWSRoute } = useRoutes(); + const { + http, + externalUrl: { getWorkplaceSearchUrl }, + } = useContext(KibanaContext) as IKibanaContext; const onClick = () => sendTelemetry({ @@ -53,7 +54,7 @@ export const OnboardingCard: React.FC = ({ const buttonActionProps = actionPath ? { onClick, - href: getWSRoute(actionPath), + href: getWorkplaceSearchUrl(actionPath), target: '_blank', 'data-test-subj': testSubj, } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx index 7fe1eae502329..d0f5893bdb88a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/onboarding_steps.tsx @@ -22,7 +22,6 @@ import { EuiLinkProps, } from '@elastic/eui'; import sharedSourcesIcon from '../shared/assets/share_circle.svg'; -import { useRoutes } from '../shared/use_routes'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; @@ -133,8 +132,10 @@ export const OnboardingSteps: React.FC = () => { }; export const OrgNameOnboarding: React.FC = () => { - const { http } = useContext(KibanaContext) as IKibanaContext; - const { getWSRoute } = useRoutes(); + const { + http, + externalUrl: { getWorkplaceSearchUrl }, + } = useContext(KibanaContext) as IKibanaContext; const onClick = () => sendTelemetry({ @@ -148,7 +149,7 @@ export const OrgNameOnboarding: React.FC = () => { onClick, target: '_blank', color: 'primary', - href: getWSRoute(ORG_SETTINGS_PATH), + href: getWorkplaceSearchUrl(ORG_SETTINGS_PATH), 'data-test-subj': 'orgNameChangeButton', } as EuiButtonEmptyProps & EuiLinkProps; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx index 2c3e78b404d42..b816eb2973207 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/overview.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +// TODO: Remove EuiPage & EuiPageBody before exposing full app + import React, { useContext, useEffect } from 'react'; import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx index 2c0fbe1275cbf..0f4f6c65d083c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/recent_activity.tsx @@ -13,7 +13,6 @@ import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiSpacer, EuiLinkProps } from '@ela import { FormattedMessage } from '@kbn/i18n/react'; import { ContentSection } from '../shared/content_section'; -import { useRoutes } from '../shared/use_routes'; import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; import { getSourcePath } from '../../routes'; @@ -92,8 +91,10 @@ export const RecentActivityItem: React.FC = ({ timestamp, sourceId, }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; - const { getWSRoute } = useRoutes(); + const { + http, + externalUrl: { getWorkplaceSearchUrl }, + } = useContext(KibanaContext) as IKibanaContext; const onClick = () => sendTelemetry({ @@ -106,7 +107,7 @@ export const RecentActivityItem: React.FC = ({ const linkProps = { onClick, target: '_blank', - href: getWSRoute(getSourcePath(sourceId)), + href: getWorkplaceSearchUrl(getSourcePath(sourceId)), external: true, color: status === 'error' ? 'danger' : 'primary', 'data-test-subj': 'viewSourceDetailsLink', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.tsx index 9bc8f4f768073..3e1d285698c0c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/overview/statistic_card.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useContext } from 'react'; import { EuiCard, EuiFlexItem, EuiTitle, EuiTextColor } from '@elastic/eui'; -import { useRoutes } from '../shared/use_routes'; +import { KibanaContext, IKibanaContext } from '../../../index'; interface IStatisticCardProps { title: string; @@ -17,11 +17,13 @@ interface IStatisticCardProps { } export const StatisticCard: React.FC = ({ title, count = 0, actionPath }) => { - const { getWSRoute } = useRoutes(); + const { + externalUrl: { getWorkplaceSearchUrl }, + } = useContext(KibanaContext) as IKibanaContext; const linkProps = actionPath ? { - href: getWSRoute(actionPath), + href: getWorkplaceSearchUrl(actionPath), target: '_blank', rel: 'noopener', } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx index 5b86e14132e0f..a914000654165 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx @@ -13,14 +13,17 @@ import { sendTelemetry } from '../../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../../index'; export const ProductButton: React.FC = () => { - const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext; + const { + externalUrl: { getWorkplaceSearchUrl }, + http, + } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { fill: true, iconType: 'popout', 'data-test-subj': 'launchButton', } as EuiButtonProps & EuiLinkProps; - buttonProps.href = `${enterpriseSearchUrl}/ws`; + buttonProps.href = getWorkplaceSearchUrl(); buttonProps.target = '_blank'; buttonProps.onClick = () => sendTelemetry({ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/use_routes/use_routes.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/use_routes/use_routes.tsx deleted file mode 100644 index 48b8695f82b43..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/use_routes/use_routes.tsx +++ /dev/null @@ -1,15 +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 { useContext } from 'react'; - -import { KibanaContext, IKibanaContext } from '../../../../index'; - -export const useRoutes = () => { - const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; - const getWSRoute = (path: string): string => `${enterpriseSearchUrl}/ws${path}`; - return { getWSRoute }; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 743080d965c36..a55ff64014130 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -10,24 +10,25 @@ import React, { useContext } from 'react'; import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; -import { SetupGuide } from './components/setup_guide'; import { Overview } from './components/overview'; import { WorkplaceSearch } from './'; -describe('Workplace Search Routes', () => { +describe('Workplace Search', () => { describe('/', () => { it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => { - (useContext as jest.Mock).mockImplementationOnce(() => ({ enterpriseSearchUrl: '' })); + (useContext as jest.Mock).mockImplementationOnce(() => ({ + config: { host: '' }, + })); const wrapper = shallow(); expect(wrapper.find(Redirect)).toHaveLength(1); expect(wrapper.find(Overview)).toHaveLength(0); }); - it('renders Engine Overview when enterpriseSearchUrl is set', () => { + it('renders the Overview when enterpriseSearchUrl is set', () => { (useContext as jest.Mock).mockImplementationOnce(() => ({ - enterpriseSearchUrl: 'https://foo.bar', + config: { host: 'https://foo.bar' }, })); const wrapper = shallow(); @@ -35,12 +36,4 @@ describe('Workplace Search Routes', () => { expect(wrapper.find(Redirect)).toHaveLength(0); }); }); - - describe('/setup_guide', () => { - it('renders', () => { - const wrapper = shallow(); - - expect(wrapper.find(SetupGuide)).toHaveLength(1); - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index cfa70ea29eca8..ca0d395c0d673 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -5,7 +5,7 @@ */ import React, { useContext } from 'react'; -import { Route, Redirect } from 'react-router-dom'; +import { Route, Redirect, Switch } from 'react-router-dom'; import { Provider } from 'react-redux'; import { Store } from 'redux'; import { getContext, resetContext } from 'kea'; @@ -15,6 +15,8 @@ resetContext({ createStore: true }); const store = getContext().store as Store; import { KibanaContext, IKibanaContext } from '../index'; +import { Layout } from '../shared/layout'; +import { WorkplaceSearchNav } from './components/layout/nav'; import { SETUP_GUIDE_PATH } from './routes'; @@ -22,15 +24,40 @@ import { SetupGuide } from './components/setup_guide'; import { Overview } from './components/overview'; export const WorkplaceSearch: React.FC = () => { - const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext; + const { config } = useContext(KibanaContext) as IKibanaContext; + if (!config.host) + return ( + + + + + + + {/* Kibana displays a blank page on redirect if this isn't included */} + + + ); + return ( - - {!enterpriseSearchUrl ? : } - - - - + + + + + + + + + }> + + + {/* Will replace with groups component subsequent PR */} +
+ + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index d9798d1f30cfc..993a1a378e738 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -4,9 +4,107 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ORG_SOURCES_PATH = '/org/sources'; -export const USERS_PATH = '/org/users'; -export const ORG_SETTINGS_PATH = '/org/settings'; +import { CURRENT_MAJOR_VERSION } from '../../../common/version'; + export const SETUP_GUIDE_PATH = '/setup_guide'; +export const LEAVE_FEEDBACK_EMAIL = 'support@elastic.co'; +export const LEAVE_FEEDBACK_URL = `mailto:${LEAVE_FEEDBACK_EMAIL}?Subject=Elastic%20Workplace%20Search%20Feedback`; + +export const DOCS_PREFIX = `https://www.elastic.co/guide/en/workplace-search/${CURRENT_MAJOR_VERSION}`; +export const ENT_SEARCH_DOCS_PREFIX = `https://www.elastic.co/guide/en/enterprise-search/${CURRENT_MAJOR_VERSION}`; +export const DOCUMENT_PERMISSIONS_DOCS_URL = `${DOCS_PREFIX}/workplace-search-sources-document-permissions.html`; +export const DOCUMENT_PERMISSIONS_SYNC_DOCS_URL = `${DOCUMENT_PERMISSIONS_DOCS_URL}#sources-permissions-synchronizing`; +export const PRIVATE_SOURCES_DOCS_URL = `${DOCUMENT_PERMISSIONS_DOCS_URL}#sources-permissions-org-private`; +export const EXTERNAL_IDENTITIES_DOCS_URL = `${DOCS_PREFIX}/workplace-search-external-identities-api.html`; +export const SECURITY_DOCS_URL = `${DOCS_PREFIX}/workplace-search-security.html`; +export const SMTP_DOCS_URL = `${DOCS_PREFIX}/workplace-search-smtp-mailer.html`; +export const CONFLUENCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-confluence-cloud-connector.html`; +export const CONFLUENCE_SERVER_DOCS_URL = `${DOCS_PREFIX}/workplace-search-confluence-server-connector.html`; +export const DROPBOX_DOCS_URL = `${DOCS_PREFIX}/workplace-search-dropbox-connector.html`; +export const GITHUB_DOCS_URL = `${DOCS_PREFIX}/workplace-search-github-connector.html`; +export const GITHUB_ENTERPRISE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-github-connector.html`; +export const GMAIL_DOCS_URL = `${DOCS_PREFIX}/workplace-search-gmail-connector.html`; +export const GOOGLE_DRIVE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-google-drive-connector.html`; +export const JIRA_DOCS_URL = `${DOCS_PREFIX}/workplace-search-jira-cloud-connector.html`; +export const JIRA_SERVER_DOCS_URL = `${DOCS_PREFIX}/workplace-search-jira-server-connector.html`; +export const ONE_DRIVE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-onedrive-connector.html`; +export const SALESFORCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-salesforce-connector.html`; +export const SERVICE_NOW_DOCS_URL = `${DOCS_PREFIX}/workplace-search-servicenow-connector.html`; +export const SHARE_POINT_DOCS_URL = `${DOCS_PREFIX}/workplace-search-sharepoint-online-connector.html`; +export const SLACK_DOCS_URL = `${DOCS_PREFIX}/workplace-search-slack-connector.html`; +export const ZENDESK_DOCS_URL = `${DOCS_PREFIX}/workplace-search-zendesk-connector.html`; +export const CUSTOM_SOURCE_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-api-sources.html`; +export const CUSTOM_API_DOCS_URL = `${DOCS_PREFIX}/workplace-search-custom-sources-api.html`; +export const CUSTOM_API_DOCUMENT_PERMISSIONS_DOCS_URL = `${CUSTOM_SOURCE_DOCS_URL}#custom-api-source-document-level-access-control`; +export const ENT_SEARCH_LICENSE_MANAGEMENT = `${ENT_SEARCH_DOCS_PREFIX}/license-management.html`; + +export const ORG_PATH = '/org'; + +export const ROLE_MAPPINGS_PATH = `${ORG_PATH}/role-mappings`; +export const ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/:roleId`; +export const ROLE_MAPPING_NEW_PATH = `${ROLE_MAPPINGS_PATH}/new`; + +export const USERS_PATH = `${ORG_PATH}/users`; +export const SECURITY_PATH = `${ORG_PATH}/security`; + +export const GROUPS_PATH = `${ORG_PATH}/groups`; +export const GROUP_PATH = `${GROUPS_PATH}/:groupId`; +export const GROUP_SOURCE_PRIORITIZATION_PATH = `${GROUPS_PATH}/:groupId/source-prioritization`; + +export const SOURCES_PATH = '/sources'; +export const ORG_SOURCES_PATH = `${ORG_PATH}${SOURCES_PATH}`; + +export const SOURCE_ADDED_PATH = `${SOURCES_PATH}/added`; +export const ADD_SOURCE_PATH = `${SOURCES_PATH}/add`; +export const ADD_CONFLUENCE_PATH = `${SOURCES_PATH}/add/confluence-cloud`; +export const ADD_CONFLUENCE_SERVER_PATH = `${SOURCES_PATH}/add/confluence-server`; +export const ADD_DROPBOX_PATH = `${SOURCES_PATH}/add/dropbox`; +export const ADD_GITHUB_ENTERPRISE_PATH = `${SOURCES_PATH}/add/github-enterprise-server`; +export const ADD_GITHUB_PATH = `${SOURCES_PATH}/add/github`; +export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`; +export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google-drive`; +export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira-cloud`; +export const ADD_JIRA_SERVER_PATH = `${SOURCES_PATH}/add/jira-server`; +export const ADD_ONE_DRIVE_PATH = `${SOURCES_PATH}/add/one-drive`; +export const ADD_SALESFORCE_PATH = `${SOURCES_PATH}/add/salesforce`; +export const ADD_SERVICE_NOW_PATH = `${SOURCES_PATH}/add/service-now`; +export const ADD_SHARE_POINT_PATH = `${SOURCES_PATH}/add/share-point`; +export const ADD_SLACK_PATH = `${SOURCES_PATH}/add/slack`; +export const ADD_ZENDESK_PATH = `${SOURCES_PATH}/add/zendesk`; +export const ADD_CUSTOM_PATH = `${SOURCES_PATH}/add/custom`; + +export const PERSONAL_SETTINGS_PATH = '/settings'; + +export const SOURCE_DETAILS_PATH = `${SOURCES_PATH}/:sourceId`; +export const SOURCE_CONTENT_PATH = `${SOURCES_PATH}/:sourceId/content`; +export const SOURCE_SCHEMAS_PATH = `${SOURCES_PATH}/:sourceId/schemas`; +export const SOURCE_DISPLAY_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/display-settings`; +export const SOURCE_SETTINGS_PATH = `${SOURCES_PATH}/:sourceId/settings`; +export const REINDEX_JOB_PATH = `${SOURCES_PATH}/:sourceId/schema-errors/:activeReindexJobId`; + +export const DISPLAY_SETTINGS_SEARCH_RESULT_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/`; +export const DISPLAY_SETTINGS_RESULT_DETAIL_PATH = `${SOURCE_DISPLAY_SETTINGS_PATH}/result-detail`; + +export const ORG_SETTINGS_PATH = `${ORG_PATH}/settings`; +export const ORG_SETTINGS_CUSTOMIZE_PATH = `${ORG_SETTINGS_PATH}/customize`; +export const ORG_SETTINGS_CONNECTORS_PATH = `${ORG_SETTINGS_PATH}/connectors`; +export const ORG_SETTINGS_OAUTH_APPLICATION_PATH = `${ORG_SETTINGS_PATH}/oauth`; +export const EDIT_CONFLUENCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence-cloud/edit`; +export const EDIT_CONFLUENCE_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/confluence-server/edit`; +export const EDIT_DROPBOX_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/dropbox/edit`; +export const EDIT_GITHUB_ENTERPRISE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github-enterprise-server/edit`; +export const EDIT_GITHUB_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/github/edit`; +export const EDIT_GMAIL_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/gmail/edit`; +export const EDIT_GOOGLE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/google-drive/edit`; +export const EDIT_JIRA_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira-cloud/edit`; +export const EDIT_JIRA_SERVER_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/jira-server/edit`; +export const EDIT_ONE_DRIVE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/one-drive/edit`; +export const EDIT_SALESFORCE_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/salesforce/edit`; +export const EDIT_SERVICE_NOW_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/service-now/edit`; +export const EDIT_SHARE_POINT_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/share-point/edit`; +export const EDIT_SLACK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/slack/edit`; +export const EDIT_ZENDESK_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/zendesk/edit`; +export const EDIT_CUSTOM_PATH = `${ORG_SETTINGS_CONNECTORS_PATH}/custom/edit`; + export const getSourcePath = (sourceId: string): string => `${ORG_SOURCES_PATH}/${sourceId}`; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 42ad7de93b00e..0d392eefe0aa2 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -21,13 +21,21 @@ import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { LicensingPluginSetup } from '../../licensing/public'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../common/constants'; -import { getPublicUrl } from './applications/shared/enterprise_search_url'; +import { + getPublicUrl, + ExternalUrl, + IExternalUrl, +} from './applications/shared/enterprise_search_url'; import AppSearchLogo from './applications/app_search/assets/logo.svg'; import WorkplaceSearchLogo from './applications/workplace_search/assets/logo.svg'; export interface ClientConfigType { host?: string; } +export interface ClientData { + externalUrl: IExternalUrl; +} + export interface PluginsSetup { home: HomePublicPluginSetup; licensing: LicensingPluginSetup; @@ -35,15 +43,15 @@ export interface PluginsSetup { export class EnterpriseSearchPlugin implements Plugin { private config: ClientConfigType; - private hasCheckedPublicUrl: boolean = false; + private hasInitialized: boolean = false; + private data: ClientData = {} as ClientData; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); + this.data.externalUrl = new ExternalUrl(this.config.host || ''); } public setup(core: CoreSetup, plugins: PluginsSetup) { - const config = { host: this.config.host }; - core.application.register({ id: APP_SEARCH_PLUGIN.ID, title: APP_SEARCH_PLUGIN.NAME, @@ -54,12 +62,12 @@ export class EnterpriseSearchPlugin implements Plugin { const { chrome } = coreStart; chrome.docTitle.change(APP_SEARCH_PLUGIN.NAME); - await this.setPublicUrl(config, coreStart.http); + await this.getInitialData(coreStart.http); const { renderApp } = await import('./applications'); const { AppSearch } = await import('./applications/app_search'); - return renderApp(AppSearch, coreStart, params, config, plugins); + return renderApp(AppSearch, params, coreStart, plugins, this.config, this.data); }, }); @@ -73,12 +81,12 @@ export class EnterpriseSearchPlugin implements Plugin { const { chrome } = coreStart; chrome.docTitle.change(WORKPLACE_SEARCH_PLUGIN.NAME); - await this.setPublicUrl(config, coreStart.http); + await this.getInitialData(coreStart.http); const { renderApp } = await import('./applications'); const { WorkplaceSearch } = await import('./applications/workplace_search'); - return renderApp(WorkplaceSearch, coreStart, params, config, plugins); + return renderApp(WorkplaceSearch, params, coreStart, plugins, this.config, this.data); }, }); @@ -107,12 +115,14 @@ export class EnterpriseSearchPlugin implements Plugin { public stop() {} - private async setPublicUrl(config: ClientConfigType, http: HttpSetup) { - if (!config.host) return; // No API to check - if (this.hasCheckedPublicUrl) return; // We've already performed the check + private async getInitialData(http: HttpSetup) { + if (!this.config.host) return; // No API to call + if (this.hasInitialized) return; // We've already made an initial call + // TODO: Rename to something more generic once we start fetching more data than just external_url from this endpoint const publicUrl = await getPublicUrl(http); - if (publicUrl) config.host = publicUrl; - this.hasCheckedPublicUrl = true; + + if (publicUrl) this.data.externalUrl = new ExternalUrl(publicUrl); + this.hasInitialized = true; } } diff --git a/x-pack/plugins/file_upload/public/index.ts b/x-pack/plugins/file_upload/public/index.ts index 1e39fb4dc8596..71853488eba89 100644 --- a/x-pack/plugins/file_upload/public/index.ts +++ b/x-pack/plugins/file_upload/public/index.ts @@ -10,4 +10,5 @@ export function plugin() { return new FileUploadPlugin(); } +export { StartContract } from './plugin'; export { FileUploadComponentProps } from './get_file_upload_component'; diff --git a/x-pack/plugins/file_upload/public/plugin.ts b/x-pack/plugins/file_upload/public/plugin.ts index ff74be659aeca..0431e660abe88 100644 --- a/x-pack/plugins/file_upload/public/plugin.ts +++ b/x-pack/plugins/file_upload/public/plugin.ts @@ -4,33 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; -import { getFileUploadComponent } from './get_file_upload_component'; +import { FileUploadComponentProps, getFileUploadComponent } from './get_file_upload_component'; // @ts-ignore import { setupInitServicesAndConstants, startInitServicesAndConstants } from './kibana_services'; import { IDataPluginServices } from '../../../../src/plugins/data/public'; -/** - * These are the interfaces with your public contracts. You should export these - * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. - * @public - */ - // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface FileUploadPluginSetupDependencies {} -export interface FileUploadPluginStartDependencies { +export interface SetupDependencies {} +export interface StartDependencies { data: IDataPluginServices; } -export type FileUploadPluginSetup = ReturnType; -export type FileUploadPluginStart = ReturnType; +export type SetupContract = ReturnType; +export interface StartContract { + getFileUploadComponent: () => Promise>; +} -export class FileUploadPlugin implements Plugin { - public setup(core: CoreSetup, plugins: FileUploadPluginSetupDependencies) { +export class FileUploadPlugin + implements Plugin { + public setup(core: CoreSetup, plugins: SetupDependencies) { setupInitServicesAndConstants(core); } - public start(core: CoreStart, plugins: FileUploadPluginStartDependencies) { + public start(core: CoreStart, plugins: StartDependencies) { startInitServicesAndConstants(core, plugins); return { getFileUploadComponent, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/active_badge.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/active_badge.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js deleted file mode 100644 index d4605ceb43499..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.container.js +++ /dev/null @@ -1,22 +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 { connect } from 'react-redux'; - -import { getPhase } from '../../../../store/selectors'; -import { setPhaseData } from '../../../../store/actions'; -import { PHASE_COLD, PHASE_HOT, PHASE_ROLLOVER_ENABLED } from '../../../../constants'; -import { ColdPhase as PresentationComponent } from './cold_phase'; - -export const ColdPhase = connect( - (state) => ({ - phaseData: getPhase(state, PHASE_COLD), - hotPhaseRolloverEnabled: getPhase(state, PHASE_HOT)[PHASE_ROLLOVER_ENABLED], - }), - { - setPhaseData: (key, value) => setPhaseData(PHASE_COLD, key, value), - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js deleted file mode 100644 index e0d70ceb57726..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { ColdPhase } from './cold_phase.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js deleted file mode 100644 index 84bd17e3637e8..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.container.js +++ /dev/null @@ -1,21 +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 { connect } from 'react-redux'; -import { getPhase } from '../../../../store/selectors'; -import { setPhaseData } from '../../../../store/actions'; -import { PHASE_DELETE, PHASE_HOT, PHASE_ROLLOVER_ENABLED } from '../../../../constants'; -import { DeletePhase as PresentationComponent } from './delete_phase'; - -export const DeletePhase = connect( - (state) => ({ - phaseData: getPhase(state, PHASE_DELETE), - hotPhaseRolloverEnabled: getPhase(state, PHASE_HOT)[PHASE_ROLLOVER_ENABLED], - }), - { - setPhaseData: (key, value) => setPhaseData(PHASE_DELETE, key, value), - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form_errors.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/form_errors.tsx diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js deleted file mode 100644 index 5f1451afdcc31..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.container.js +++ /dev/null @@ -1,22 +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 { connect } from 'react-redux'; - -import { getPhase } from '../../../../store/selectors'; -import { setPhaseData } from '../../../../store/actions'; -import { PHASE_HOT, PHASE_WARM, WARM_PHASE_ON_ROLLOVER } from '../../../../constants'; -import { HotPhase as PresentationComponent } from './hot_phase'; - -export const HotPhase = connect( - (state) => ({ - phaseData: getPhase(state, PHASE_HOT), - }), - { - setPhaseData: (key, value) => setPhaseData(PHASE_HOT, key, value), - setWarmPhaseOnRollover: (value) => setPhaseData(PHASE_WARM, WARM_PHASE_ON_ROLLOVER, value), - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js deleted file mode 100644 index 114e34c3ef4d0..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { HotPhase } from './hot_phase.container'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts similarity index 54% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index a2ae37780b9f9..e933c46e98491 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/index.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -5,6 +5,13 @@ */ export { ActiveBadge } from './active_badge'; +export { ErrableFormRow } from './form_errors'; export { LearnMoreLink } from './learn_more_link'; -export { PhaseErrorMessage } from './phase_error_message'; +export { MinAgeInput } from './min_age_input'; +export { NodeAllocation } from './node_allocation'; +export { NodeAttrsDetails } from './node_attrs_details'; export { OptionalLabel } from './optional_label'; +export { PhaseErrorMessage } from './phase_error_message'; +export { PolicyJsonFlyout } from './policy_json_flyout'; +export { SetPriorityInput } from './set_priority_input'; +export { SnapshotPolicies } from './snapshot_policies'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/learn_more_link.tsx similarity index 92% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/learn_more_link.tsx index 623ff982438d7..5ada49b318018 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/learn_more_link.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/learn_more_link.tsx @@ -8,7 +8,7 @@ import React, { ReactNode } from 'react'; import { EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { createDocLink } from '../../services/documentation'; +import { createDocLink } from '../../../services/documentation'; interface Props { docPath: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx similarity index 91% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx index d90ad9378efd4..c9732f2311758 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/min_age_input.tsx @@ -16,10 +16,10 @@ import { PHASE_COLD, PHASE_DELETE, } from '../../../constants'; -import { LearnMoreLink } from '../../components'; -import { ErrableFormRow } from '../form_errors'; +import { LearnMoreLink } from './learn_more_link'; +import { ErrableFormRow } from './form_errors'; -function getTimingLabelForPhase(phase) { +function getTimingLabelForPhase(phase: string) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { case PHASE_WARM: @@ -39,7 +39,7 @@ function getTimingLabelForPhase(phase) { } } -function getUnitsAriaLabelForPhase(phase) { +function getUnitsAriaLabelForPhase(phase: string) { // NOTE: Hot phase isn't necessary, because indices begin in the hot phase. switch (phase) { case PHASE_WARM: @@ -68,9 +68,24 @@ function getUnitsAriaLabelForPhase(phase) { } } -export const MinAgeInput = (props) => { - const { rolloverEnabled, errors, phaseData, phase, setPhaseData, isShowingErrors } = props; +interface Props { + rolloverEnabled: boolean; + errors: Record; + phase: string; + // TODO add types for phaseData and setPhaseData after policy is typed + phaseData: any; + setPhaseData: (dataKey: string, value: any) => void; + isShowingErrors: boolean; +} +export const MinAgeInput: React.FunctionComponent = ({ + rolloverEnabled, + errors, + phaseData, + phase, + setPhaseData, + isShowingErrors, +}) => { let daysOptionLabel; let hoursOptionLabel; let minutesOptionLabel; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx similarity index 93% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 31261de45c743..576483a5ab9c2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -16,17 +16,18 @@ import { EuiButton, } from '@elastic/eui'; -import { PHASE_NODE_ATTRS } from '../../../../constants'; -import { LearnMoreLink } from '../../../components/learn_more_link'; -import { ErrableFormRow } from '../../form_errors'; -import { useLoadNodes } from '../../../../services/api'; -import { NodeAttrsDetails } from '../node_attrs_details'; +import { PHASE_NODE_ATTRS } from '../../../constants'; +import { LearnMoreLink } from './learn_more_link'; +import { ErrableFormRow } from './form_errors'; +import { useLoadNodes } from '../../../services/api'; +import { NodeAttrsDetails } from './node_attrs_details'; interface Props { phase: string; - setPhaseData: (dataKey: string, value: any) => void; - errors: any; + errors: Record; + // TODO add types for phaseData and setPhaseData after policy is typed phaseData: any; + setPhaseData: (dataKey: string, value: any) => void; isShowingErrors: boolean; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx similarity index 97% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx index 6fcbd94dc5e9a..cd87cc324a414 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/node_attrs_details.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details.tsx @@ -20,7 +20,7 @@ import { EuiButton, } from '@elastic/eui'; -import { useLoadNodeDetails } from '../../../../services/api'; +import { useLoadNodeDetails } from '../../../services/api'; interface Props { close: () => void; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.ts deleted file mode 100644 index 056d2f2f600f3..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_attrs_details/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { NodeAttrsDetails } from './node_attrs_details'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/optional_label.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/optional_label.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/optional_label.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/optional_label.tsx diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx similarity index 87% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx index 904ac7c25f2f9..750f68543f221 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/components/phase_error_message.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_error_message.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiBadge } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -export const PhaseErrorMessage = ({ isShowingErrors }) => { +export const PhaseErrorMessage = ({ isShowingErrors }: { isShowingErrors: boolean }) => { return isShowingErrors ? ( '}`; - const request = `${endpoint}\n${this.getEsJson(lifecycle)}`; - - return ( - - - -

- {policyName ? ( - - ) : ( - - )} -

-
-
- - - -

- -

-
- - - - - {request} - -
- - - - - - -
- ); - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx new file mode 100644 index 0000000000000..aaf4aa6e6222d --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/policy_json_flyout.tsx @@ -0,0 +1,98 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; + +import { + EuiButtonEmpty, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +interface Props { + close: () => void; + // TODO add types for lifecycle after policy is typed + lifecycle: any; + policyName: string; +} + +export const PolicyJsonFlyout: React.FunctionComponent = ({ + close, + lifecycle, + policyName, +}) => { + // @ts-ignore until store is typed + const getEsJson = ({ phases }) => { + return JSON.stringify( + { + policy: { + phases, + }, + }, + null, + 2 + ); + }; + + const endpoint = `PUT _ilm/policy/${policyName || ''}`; + const request = `${endpoint}\n${getEsJson(lifecycle)}`; + + return ( + + + +

+ {policyName ? ( + + ) : ( + + )} +

+
+
+ + + +

+ +

+
+ + + + + {request} + +
+ + + + + + +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx similarity index 80% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx index bdcc1e23b4230..0034de85fce17 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/set_priority_input.tsx @@ -8,12 +8,26 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFieldNumber, EuiTextColor, EuiDescribedFormGroup } from '@elastic/eui'; import { PHASE_INDEX_PRIORITY } from '../../../constants'; -import { LearnMoreLink, OptionalLabel } from '../../components'; -import { ErrableFormRow } from '../form_errors'; -export const SetPriorityInput = (props) => { - const { errors, phaseData, phase, setPhaseData, isShowingErrors } = props; +import { LearnMoreLink } from './'; +import { OptionalLabel } from './'; +import { ErrableFormRow } from './'; +interface Props { + errors: Record; + // TODO add types for phaseData and setPhaseData after policy is typed + phase: string; + phaseData: any; + setPhaseData: (dataKey: string, value: any) => void; + isShowingErrors: boolean; +} +export const SetPriorityInput: React.FunctionComponent = ({ + errors, + phaseData, + phase, + setPhaseData, + isShowingErrors, +}) => { return ( ({ - phaseData: getPhase(state, PHASE_WARM), - hotPhaseRolloverEnabled: getPhase(state, PHASE_HOT)[PHASE_ROLLOVER_ENABLED], - }), - { - setPhaseData: (key, value) => setPhaseData(PHASE_WARM, key, value), - } -)(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js index 1c6ced8953211..e7f20a66d09f0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.js @@ -15,6 +15,7 @@ import { isPolicyListLoaded, getIsNewPolicy, getSelectedOriginalPolicyName, + getPhases, } from '../../store/selectors'; import { @@ -23,6 +24,7 @@ import { setSaveAsNewPolicy, saveLifecyclePolicy, fetchPolicies, + setPhaseData, } from '../../store/actions'; import { findFirstError } from '../../services/find_errors'; @@ -42,6 +44,7 @@ export const EditPolicy = connect( isPolicyListLoaded: isPolicyListLoaded(state), isNewPolicy: getIsNewPolicy(state), originalPolicyName: getSelectedOriginalPolicyName(state), + phases: getPhases(state), }; }, { @@ -50,5 +53,6 @@ export const EditPolicy = connect( setSaveAsNewPolicy, saveLifecyclePolicy, fetchPolicies, + setPhaseData, } )(PresentationComponent); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js index d9d8866a2e2cc..a29ecd07c5e45 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.js @@ -33,17 +33,15 @@ import { PHASE_DELETE, PHASE_WARM, STRUCTURE_POLICY_NAME, + WARM_PHASE_ON_ROLLOVER, + PHASE_ROLLOVER_ENABLED, } from '../../constants'; import { toasts } from '../../services/notification'; import { findFirstError } from '../../services/find_errors'; -import { LearnMoreLink } from '../components'; -import { PolicyJsonFlyout } from './components/policy_json_flyout'; -import { ErrableFormRow } from './form_errors'; -import { HotPhase } from './components/hot_phase'; -import { WarmPhase } from './components/warm_phase'; -import { DeletePhase } from './components/delete_phase'; -import { ColdPhase } from './components/cold_phase'; +import { LearnMoreLink, PolicyJsonFlyout, ErrableFormRow } from './components'; + +import { HotPhase, WarmPhase, ColdPhase, DeletePhase } from './phases'; export class EditPolicy extends Component { static propTypes = { @@ -137,6 +135,8 @@ export class EditPolicy extends Component { isNewPolicy, lifecycle, originalPolicyName, + phases, + setPhaseData, } = this.props; const selectedPolicyName = selectedPolicy.name; const { isShowingErrors, isShowingPolicyJsonFlyout } = this.state; @@ -275,9 +275,13 @@ export class EditPolicy extends Component { setPhaseData(PHASE_HOT, key, value)} + phaseData={phases[PHASE_HOT]} + setWarmPhaseOnRollover={(value) => + setPhaseData(PHASE_WARM, WARM_PHASE_ON_ROLLOVER, value) + } /> @@ -285,6 +289,9 @@ export class EditPolicy extends Component { setPhaseData(PHASE_WARM, key, value)} + phaseData={phases[PHASE_WARM]} + hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} /> @@ -292,6 +299,9 @@ export class EditPolicy extends Component { setPhaseData(PHASE_COLD, key, value)} + phaseData={phases[PHASE_COLD]} + hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} /> @@ -300,6 +310,9 @@ export class EditPolicy extends Component { errors={errors[PHASE_DELETE]} isShowingErrors={isShowingErrors && !!findFirstError(errors[PHASE_DELETE], false)} getUrlForApp={this.props.getUrlForApp} + setPhaseData={(key, value) => setPhaseData(PHASE_DELETE, key, value)} + phaseData={phases[PHASE_DELETE]} + hotPhaseRolloverEnabled={phases[PHASE_HOT][PHASE_ROLLOVER_ENABLED]} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx similarity index 92% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx index 200bf0e767d9d..babbbf7638ebe 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/cold_phase/cold_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/cold_phase.tsx @@ -5,7 +5,6 @@ */ import React, { PureComponent, Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -24,20 +23,27 @@ import { PHASE_ENABLED, PHASE_REPLICA_COUNT, PHASE_FREEZE_ENABLED, -} from '../../../../constants'; -import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, OptionalLabel } from '../../../components'; -import { ErrableFormRow } from '../../form_errors'; -import { MinAgeInput } from '../min_age_input'; -import { NodeAllocation } from '../node_allocation'; -import { SetPriorityInput } from '../set_priority_input'; +} from '../../../constants'; +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + OptionalLabel, + ErrableFormRow, + MinAgeInput, + NodeAllocation, + SetPriorityInput, +} from '../components'; -export class ColdPhase extends PureComponent { - static propTypes = { - setPhaseData: PropTypes.func.isRequired, +interface Props { + setPhaseData: (key: string, value: any) => void; + phaseData: any; + isShowingErrors: boolean; + errors: Record; + hotPhaseRolloverEnabled: boolean; +} - isShowingErrors: PropTypes.bool.isRequired, - errors: PropTypes.object.isRequired, - }; +export class ColdPhase extends PureComponent { render() { const { setPhaseData, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx similarity index 88% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx index 2b12eec953e11..0143cc4af24e3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/delete_phase.tsx @@ -5,22 +5,35 @@ */ import React, { PureComponent, Fragment } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui'; -import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../../constants'; -import { ActiveBadge, LearnMoreLink, OptionalLabel, PhaseErrorMessage } from '../../../components'; -import { MinAgeInput } from '../min_age_input'; -import { SnapshotPolicies } from '../snapshot_policies'; +import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../constants'; +import { + ActiveBadge, + LearnMoreLink, + OptionalLabel, + PhaseErrorMessage, + MinAgeInput, + SnapshotPolicies, +} from '../components'; -export class DeletePhase extends PureComponent { - static propTypes = { - setPhaseData: PropTypes.func.isRequired, - isShowingErrors: PropTypes.bool.isRequired, - errors: PropTypes.object.isRequired, - }; +interface Props { + setPhaseData: (key: string, value: any) => void; + phaseData: any; + isShowingErrors: boolean; + errors: Record; + hotPhaseRolloverEnabled: boolean; + getUrlForApp: ( + appId: string, + options?: { + path?: string; + absolute?: boolean; + } + ) => string; +} +export class DeletePhase extends PureComponent { render() { const { setPhaseData, @@ -28,6 +41,7 @@ export class DeletePhase extends PureComponent { errors, isShowingErrors, hotPhaseRolloverEnabled, + getUrlForApp, } = this.props; return ( @@ -123,7 +137,7 @@ export class DeletePhase extends PureComponent { setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, value)} - getUrlForApp={this.props.getUrlForApp} + getUrlForApp={getUrlForApp} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx similarity index 96% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx index b420442198712..dbd48f3a85634 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/hot_phase/hot_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/hot_phase.tsx @@ -5,7 +5,6 @@ */ import React, { Fragment, PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -28,18 +27,24 @@ import { PHASE_ROLLOVER_MAX_SIZE_STORED, PHASE_ROLLOVER_MAX_SIZE_STORED_UNITS, PHASE_ROLLOVER_ENABLED, -} from '../../../../constants'; -import { LearnMoreLink, ActiveBadge, PhaseErrorMessage } from '../../../components'; -import { ErrableFormRow } from '../../form_errors'; -import { SetPriorityInput } from '../set_priority_input'; +} from '../../../constants'; +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + ErrableFormRow, + SetPriorityInput, +} from '../components'; -export class HotPhase extends PureComponent { - static propTypes = { - setPhaseData: PropTypes.func.isRequired, - isShowingErrors: PropTypes.bool.isRequired, - errors: PropTypes.object.isRequired, - }; +interface Props { + errors: Record; + isShowingErrors: boolean; + phaseData: any; + setPhaseData: (key: string, value: any) => void; + setWarmPhaseOnRollover: (value: boolean) => void; +} +export class HotPhase extends PureComponent { render() { const { setPhaseData, phaseData, isShowingErrors, errors, setWarmPhaseOnRollover } = this.props; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts similarity index 58% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts index 4675ab46ee501..8d1ace5950497 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/index.ts @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { NodeAllocation } from './node_allocation'; +export { HotPhase } from './hot_phase'; +export { WarmPhase } from './warm_phase'; +export { ColdPhase } from './cold_phase'; +export { DeletePhase } from './delete_phase'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx similarity index 95% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx index 60b5ab4781b6d..6ed81bf8f45d5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/warm_phase/warm_phase.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/phases/warm_phase.tsx @@ -5,7 +5,6 @@ */ import React, { Fragment, PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { @@ -28,21 +27,26 @@ import { PHASE_PRIMARY_SHARD_COUNT, PHASE_REPLICA_COUNT, PHASE_SHRINK_ENABLED, -} from '../../../../constants'; -import { LearnMoreLink, ActiveBadge, PhaseErrorMessage, OptionalLabel } from '../../../components'; -import { ErrableFormRow } from '../../form_errors'; -import { SetPriorityInput } from '../set_priority_input'; -import { NodeAllocation } from '../node_allocation'; -import { MinAgeInput } from '../min_age_input'; - -export class WarmPhase extends PureComponent { - static propTypes = { - setPhaseData: PropTypes.func.isRequired, - - isShowingErrors: PropTypes.bool.isRequired, - errors: PropTypes.object.isRequired, - }; +} from '../../../constants'; +import { + LearnMoreLink, + ActiveBadge, + PhaseErrorMessage, + OptionalLabel, + ErrableFormRow, + SetPriorityInput, + NodeAllocation, + MinAgeInput, +} from '../components'; +interface Props { + setPhaseData: (key: string, value: any) => void; + phaseData: any; + isShowingErrors: boolean; + errors: Record; + hotPhaseRolloverEnabled: boolean; +} +export class WarmPhase extends PureComponent { render() { const { setPhaseData, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js index 8e53569047d8f..47134ad097720 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/add_policy_to_template_confirm_modal.js @@ -23,7 +23,7 @@ import { import { toasts } from '../../../../services/notification'; import { addLifecyclePolicyToTemplate, loadIndexTemplates } from '../../../../services/api'; import { showApiError } from '../../../../services/api_errors'; -import { LearnMoreLink } from '../../../components/learn_more_link'; +import { LearnMoreLink } from '../../../edit_policy/components'; export class AddPolicyToTemplateConfirmModal extends Component { state = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js index 3f1c00db621a7..45a8e63f70e83 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js +++ b/x-pack/plugins/index_lifecycle_management/public/application/store/actions/nodes.js @@ -4,8 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ import { createAction } from 'redux-actions'; -import { SET_SELECTED_NODE_ATTRS } from '../../constants'; - -export const setSelectedNodeAttrs = createAction(SET_SELECTED_NODE_ATTRS); export const setSelectedPrimaryShardCount = createAction('SET_SELECTED_PRIMARY_SHARED_COUNT'); export const setSelectedReplicaCount = createAction('SET_SELECTED_REPLICA_COUNT'); diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index 2d53203a60e4f..5077bccdc1ca2 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -8,7 +8,7 @@ import React, { memo, useState, useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; -import { euiStyled } from '../../../../../observability/public'; +import { euiStyled, useUiTracker } from '../../../../../observability/public'; import { isTimestampColumn } from '../../../utils/log_entry'; import { LogColumnConfiguration, @@ -68,6 +68,8 @@ export const LogEntryRow = memo( scale, wrap, }: LogEntryRowProps) => { + const trackMetric = useUiTracker({ app: 'infra_logs' }); + const [isHovered, setIsHovered] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -82,10 +84,10 @@ export const LogEntryRow = memo( logEntry.id, ]); - const handleOpenViewLogInContext = useCallback(() => openViewLogInContext?.(logEntry), [ - openViewLogInContext, - logEntry, - ]); + const handleOpenViewLogInContext = useCallback(() => { + openViewLogInContext?.(logEntry); // eslint-disable-line no-unused-expressions + trackMetric({ metric: 'view_in_context__stream' }); + }, [openViewLogInContext, logEntry, trackMetric]); const hasContext = useMemo(() => !isEmpty(logEntry.context), [logEntry]); const hasActionFlyoutWithItem = openFlyoutWithItem !== undefined; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx index 908e52f01cbcc..7bcc05280994c 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx @@ -26,6 +26,7 @@ import { import { LogColumnConfiguration } from '../../../../../utils/source_configuration'; import { LogEntryContextMenu } from '../../../../../components/logging/log_text_stream/log_entry_context_menu'; import { useLinkProps } from '../../../../../hooks/use_link_props'; +import { useUiTracker } from '../../../../../../../observability/public'; export const exampleMessageScale = 'medium' as const; export const exampleTimestampFormat = 'dateTime' as const; @@ -39,6 +40,7 @@ export const CategoryExampleMessage: React.FunctionComponent<{ tiebreaker: number; context: LogEntryContext; }> = ({ id, dataset, message, timestamp, timeRange, tiebreaker, context }) => { + const trackMetric = useUiTracker({ app: 'infra_logs' }); const [, { setContextEntry }] = useContext(ViewLogInContext.Context); // handle special cases for the dataset value const humanFriendlyDataset = getFriendlyNameForPartitionId(dataset); @@ -129,6 +131,7 @@ export const CategoryExampleMessage: React.FunctionComponent<{ cursor: { time: timestamp, tiebreaker }, columns: [], }; + trackMetric({ metric: 'view_in_context__categories' }); setContextEntry(logEntry); }, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts index 6450f7303dd88..4f2c7c4c339f1 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/index.ts @@ -6,4 +6,4 @@ export { installPipelines } from './install'; -export { deletePipelines, deletePipeline } from './remove'; +export { deletePreviousPipelines, deletePipeline } from './remove'; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts index 8be3a1beab392..836b53b5a9225 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/remove.ts @@ -8,24 +8,32 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { appContextService } from '../../../'; import { CallESAsCurrentUser, ElasticsearchAssetType } from '../../../../types'; import { getInstallation } from '../../packages/get'; -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; +import { PACKAGES_SAVED_OBJECT_TYPE, EsAssetReference } from '../../../../../common'; -export const deletePipelines = async ( +export const deletePreviousPipelines = async ( callCluster: CallESAsCurrentUser, savedObjectsClient: SavedObjectsClientContract, pkgName: string, - pkgVersion: string + previousPkgVersion: string ) => { const logger = appContextService.getLogger(); - const previousPipelinesPattern = `*-${pkgName}.*-${pkgVersion}`; - + const installation = await getInstallation({ savedObjectsClient, pkgName }); + if (!installation) return; + const installedEsAssets = installation.installed_es; + const installedPipelines = installedEsAssets.filter( + ({ type, id }) => + type === ElasticsearchAssetType.ingestPipeline && id.includes(previousPkgVersion) + ); + const deletePipelinePromises = installedPipelines.map(({ type, id }) => { + return deletePipeline(callCluster, id); + }); try { - await deletePipeline(callCluster, previousPipelinesPattern); + await Promise.all(deletePipelinePromises); } catch (e) { logger.error(e); } try { - await deletePipelineRefs(savedObjectsClient, pkgName, pkgVersion); + await deletePipelineRefs(savedObjectsClient, installedEsAssets, pkgName, previousPkgVersion); } catch (e) { logger.error(e); } @@ -33,12 +41,10 @@ export const deletePipelines = async ( export const deletePipelineRefs = async ( savedObjectsClient: SavedObjectsClientContract, + installedEsAssets: EsAssetReference[], pkgName: string, pkgVersion: string ) => { - const installation = await getInstallation({ savedObjectsClient, pkgName }); - if (!installation) return; - const installedEsAssets = installation.installed_es; const filteredAssets = installedEsAssets.filter(({ type, id }) => { if (type !== ElasticsearchAssetType.ingestPipeline) return true; if (!id.includes(pkgVersion)) return true; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 0911aaf248e7a..6bc461845f124 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -22,7 +22,7 @@ import * as Registry from '../registry'; import { getInstallation, getInstallationObject, isRequiredPackage } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; -import { installPipelines, deletePipelines } from '../elasticsearch/ingest_pipeline/'; +import { installPipelines, deletePreviousPipelines } from '../elasticsearch/ingest_pipeline/'; import { installILMPolicy } from '../elasticsearch/ilm/install'; import { installKibanaAssets, @@ -183,7 +183,7 @@ export async function installPackage({ // if this is an update, delete the previous version's pipelines if (installedPkg && !reinstall) { - await deletePipelines( + await deletePreviousPipelines( callCluster, savedObjectsClient, pkgName, diff --git a/x-pack/plugins/maps/common/descriptor_types/index.ts b/x-pack/plugins/maps/common/descriptor_types/index.ts index b0ae065856a5d..fb47344ab32db 100644 --- a/x-pack/plugins/maps/common/descriptor_types/index.ts +++ b/x-pack/plugins/maps/common/descriptor_types/index.ts @@ -5,6 +5,7 @@ */ export * from './data_request_descriptor_types'; -export * from './sources'; +export * from './source_descriptor_types'; +export * from './layer_descriptor_types'; export * from './map_descriptor'; export * from './style_property_descriptor_types'; diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts new file mode 100644 index 0000000000000..a04d0e1a978fd --- /dev/null +++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { Query } from 'src/plugins/data/public'; +import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types'; +import { DataRequestDescriptor } from './data_request_descriptor_types'; +import { AbstractSourceDescriptor, ESTermSourceDescriptor } from './source_descriptor_types'; + +export type JoinDescriptor = { + leftField?: string; + right: ESTermSourceDescriptor; +}; + +export type LayerDescriptor = { + __dataRequests?: DataRequestDescriptor[]; + __isInErrorState?: boolean; + __isPreviewLayer?: boolean; + __errorMessage?: string; + __trackedLayerDescriptor?: LayerDescriptor; + alpha?: number; + id: string; + joins?: JoinDescriptor[]; + label?: string | null; + areLabelsOnTop?: boolean; + minZoom?: number; + maxZoom?: number; + sourceDescriptor: AbstractSourceDescriptor | null; + type?: string; + visible?: boolean; + style?: StyleDescriptor | null; + query?: Query; +}; + +export type VectorLayerDescriptor = LayerDescriptor & { + style: VectorStyleDescriptor; +}; + +export type RangeFieldMeta = { + min: number; + max: number; + delta: number; + isMinOutsideStdRange?: boolean; + isMaxOutsideStdRange?: boolean; +}; + +export type Category = { + key: string; + count: number; +}; + +export type CategoryFieldMeta = { + categories: Category[]; +}; + +export type GeometryTypes = { + isPointsOnly: boolean; + isLinesOnly: boolean; + isPolygonsOnly: boolean; +}; + +export type StyleMetaDescriptor = { + geometryTypes?: GeometryTypes; + fieldMeta: { + [key: string]: { + range: RangeFieldMeta; + categories: CategoryFieldMeta; + }; + }; +}; diff --git a/x-pack/plugins/maps/common/descriptor_types/sources.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts similarity index 65% rename from x-pack/plugins/maps/common/descriptor_types/sources.ts rename to x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index 6e8884d942e19..400b6a41ead71 100644 --- a/x-pack/plugins/maps/common/descriptor_types/sources.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -15,8 +15,6 @@ import { SCALING_TYPES, MVT_FIELD_TYPE, } from '../constants'; -import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types'; -import { DataRequestDescriptor } from './data_request_descriptor_types'; export type AttributionDescriptor = { attributionText?: string; @@ -136,83 +134,3 @@ export type GeojsonFileSourceDescriptor = { name: string; type: string; }; - -export type JoinDescriptor = { - leftField?: string; - right: ESTermSourceDescriptor; -}; - -// todo : this union type is incompatible with dynamic extensibility of sources. -// Reconsider using SourceDescriptor in type signatures for top-level classes -export type SourceDescriptor = - | AbstractSourceDescriptor - | XYZTMSSourceDescriptor - | WMSSourceDescriptor - | KibanaTilemapSourceDescriptor - | KibanaRegionmapSourceDescriptor - | ESTermSourceDescriptor - | ESSearchSourceDescriptor - | ESGeoGridSourceDescriptor - | EMSFileSourceDescriptor - | ESPewPewSourceDescriptor - | TiledSingleLayerVectorSourceDescriptor - | EMSTMSSourceDescriptor - | EMSFileSourceDescriptor - | GeojsonFileSourceDescriptor; - -export type LayerDescriptor = { - __dataRequests?: DataRequestDescriptor[]; - __isInErrorState?: boolean; - __isPreviewLayer?: boolean; - __errorMessage?: string; - __trackedLayerDescriptor?: LayerDescriptor; - alpha?: number; - id: string; - joins?: JoinDescriptor[]; - label?: string | null; - areLabelsOnTop?: boolean; - minZoom?: number; - maxZoom?: number; - sourceDescriptor: SourceDescriptor | null; - type?: string; - visible?: boolean; - style?: StyleDescriptor | null; - query?: Query; -}; - -export type VectorLayerDescriptor = LayerDescriptor & { - style?: VectorStyleDescriptor; -}; - -export type RangeFieldMeta = { - min: number; - max: number; - delta: number; - isMinOutsideStdRange?: boolean; - isMaxOutsideStdRange?: boolean; -}; - -export type Category = { - key: string; - count: number; -}; - -export type CategoryFieldMeta = { - categories: Category[]; -}; - -export type GeometryTypes = { - isPointsOnly: boolean; - isLinesOnly: boolean; - isPolygonsOnly: boolean; -}; - -export type StyleMetaDescriptor = { - geometryTypes?: GeometryTypes; - fieldMeta: { - [key: string]: { - range: RangeFieldMeta; - categories: CategoryFieldMeta; - }; - }; -}; diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js index d860f413df27b..50e583f00ae81 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.js @@ -11,7 +11,7 @@ jest.mock('./data_request_actions', () => { }; }); -import { mapExtentChanged, setMouseCoordinates } from './map_actions'; +import { mapExtentChanged, setMouseCoordinates, setQuery } from './map_actions'; const getStoreMock = jest.fn(); const dispatchMock = jest.fn(); @@ -226,4 +226,95 @@ describe('map_actions', () => { }); }); }); + + describe('setQuery', () => { + const query = { + language: 'kuery', + query: '', + queryLastTriggeredAt: '2020-08-14T15:07:12.276Z', + }; + const timeFilters = { from: 'now-1y', to: 'now' }; + const filters = [ + { + meta: { + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'extension', + params: { query: 'png' }, + }, + query: { match_phrase: { extension: 'png' } }, + $state: { store: 'appState' }, + }, + ]; + + beforeEach(() => { + //Mocks the "previous" state + require('../selectors/map_selectors').getQuery = () => { + return query; + }; + require('../selectors/map_selectors').getTimeFilters = () => { + return timeFilters; + }; + require('../selectors/map_selectors').getFilters = () => { + return filters; + }; + require('../selectors/map_selectors').getMapSettings = () => { + return { + autoFitToDataBounds: false, + }; + }; + }); + + it('should dispatch query action and resync when query changes', async () => { + const newQuery = { + language: 'kuery', + query: 'foobar', + queryLastTriggeredAt: '2020-08-14T15:07:12.276Z', + }; + const setQueryAction = await setQuery({ + query: newQuery, + filters, + }); + await setQueryAction(dispatchMock, getStoreMock); + + expect(dispatchMock.mock.calls).toEqual([ + [ + { + timeFilters, + query: newQuery, + filters, + type: 'SET_QUERY', + }, + ], + [undefined], // dispatch(syncDataForAllLayers()); + ]); + }); + + it('should not dispatch query action when nothing changes', async () => { + const setQueryAction = await setQuery({ + timeFilters, + query, + filters, + }); + await setQueryAction(dispatchMock, getStoreMock); + + expect(dispatchMock.mock.calls.length).toEqual(0); + }); + + it('should dispatch query action when nothing changes and force refresh', async () => { + const setQueryAction = await setQuery({ + timeFilters, + query, + filters, + forceRefresh: true, + }); + await setQueryAction(dispatchMock, getStoreMock); + + // Only checking calls length instead of calls because queryLastTriggeredAt changes on this run + expect(dispatchMock.mock.calls.length).toEqual(2); + }); + }); }); diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 08826276c12ad..7ba58307e1952 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; import { Dispatch } from 'redux'; import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; @@ -204,12 +205,12 @@ export function setQuery({ query, timeFilters, filters = [], - refresh = false, + forceRefresh = false, }: { - filters: Filter[]; + filters?: Filter[]; query?: Query; timeFilters?: TimeRange; - refresh?: boolean; + forceRefresh?: boolean; }) { return async (dispatch: Dispatch, getState: () => MapStoreState) => { const prevQuery = getQuery(getState()); @@ -218,15 +219,30 @@ export function setQuery({ ? prevQuery.queryLastTriggeredAt : generateQueryTimestamp(); - dispatch({ - type: SET_QUERY, + const nextQueryContext = { timeFilters: timeFilters ? timeFilters : getTimeFilters(getState()), query: { ...(query ? query : getQuery(getState())), // ensure query changes to trigger re-fetch when "Refresh" clicked - queryLastTriggeredAt: refresh ? generateQueryTimestamp() : prevTriggeredAt, + queryLastTriggeredAt: forceRefresh ? generateQueryTimestamp() : prevTriggeredAt, }, filters: filters ? filters : getFilters(getState()), + }; + + const prevQueryContext = { + timeFilters: getTimeFilters(getState()), + query: getQuery(getState()), + filters: getFilters(getState()), + }; + + if (_.isEqual(nextQueryContext, prevQueryContext)) { + // do nothing if query context has not changed + return; + } + + dispatch({ + type: SET_QUERY, + ...nextQueryContext, }); if (getMapSettings(getState()).autoFitToDataBounds) { diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index c53a7a4facb0c..8d4d57e524276 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -17,7 +17,7 @@ import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; export const emsBoundariesLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], - checkVisibility: () => { + checkVisibility: async () => { return getIsEmsEnabled(); }, description: i18n.translate('xpack.maps.source.emsFileDescription', { diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 5cc2a1225bbd7..315759a2eba29 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -18,7 +18,7 @@ import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; export const emsBaseMapLayerWizardConfig: LayerWizard = { categories: [LAYER_WIZARD_CATEGORY.REFERENCE], - checkVisibility: () => { + checkVisibility: async () => { return getIsEmsEnabled(); }, description: i18n.translate('xpack.maps.source.emsTileDescription', { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts index 2707b2ac23e58..37193e148bdc7 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts @@ -8,11 +8,7 @@ import { MapExtent, MapFilters } from '../../../../common/descriptor_types'; jest.mock('../../../kibana_services'); jest.mock('ui/new_platform'); -import { - getIndexPatternService, - getSearchService, - fetchSearchSourceAndRecordWithInspector, -} from '../../../kibana_services'; +import { getIndexPatternService, getSearchService } from '../../../kibana_services'; import { ESGeoGridSource } from './es_geo_grid_source'; import { ES_GEO_FIELD_TYPE, @@ -54,6 +50,51 @@ describe('ESGeoGridSource', () => { }, {} ); + geogridSource._runEsQuery = async (args: unknown) => { + return { + took: 71, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: 748 + 683, + max_score: null, + hits: [], + }, + aggregations: { + gridSplit: { + buckets: [ + { + key: '4/4/6', + doc_count: 748, + gridCentroid: { + location: { + lat: 35.64189018148127, + lon: -82.84314106196105, + }, + count: 748, + }, + }, + { + key: '4/3/6', + doc_count: 683, + gridCentroid: { + location: { + lat: 35.24134021274211, + lon: -98.45945192042787, + }, + count: 683, + }, + }, + ], + }, + }, + }; + }; describe('getGeoJsonWithMeta', () => { let mockSearchSource: unknown; @@ -71,50 +112,6 @@ describe('ESGeoGridSource', () => { getIndexPatternService.mockReturnValue(mockIndexPatternService); // @ts-expect-error getSearchService.mockReturnValue(mockSearchService); - // @ts-expect-error - fetchSearchSourceAndRecordWithInspector.mockReturnValue({ - took: 71, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - hits: { - total: 748 + 683, - max_score: null, - hits: [], - }, - aggregations: { - gridSplit: { - buckets: [ - { - key: '4/4/6', - doc_count: 748, - gridCentroid: { - location: { - lat: 35.64189018148127, - lon: -82.84314106196105, - }, - count: 748, - }, - }, - { - key: '4/3/6', - doc_count: 683, - gridCentroid: { - location: { - lat: 35.24134021274211, - lon: -98.45945192042787, - }, - count: 683, - }, - }, - ], - }, - }, - }); }); const extent: MapExtent = { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts index 1f2985ffcc27c..01fde589dcb84 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts @@ -52,4 +52,17 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource registerCancelCallback: (requestToken: symbol, callback: () => void) => void, searchFilters: VectorSourceRequestMeta ): Promise; + _runEsQuery: ({ + requestId, + requestName, + requestDescription, + searchSource, + registerCancelCallback, + }: { + requestId: string; + requestName: string; + requestDescription: string; + searchSource: ISearchSource; + registerCancelCallback: () => void; + }) => Promise; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js index c043e6d6994ab..866e3c76c2a3f 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js @@ -7,7 +7,6 @@ import { AbstractVectorSource } from '../vector_source'; import { getAutocompleteService, - fetchSearchSourceAndRecordWithInspector, getIndexPatternService, getTimeFilter, getSearchService, @@ -20,6 +19,7 @@ import uuid from 'uuid/v4'; import { copyPersistentState } from '../../../reducers/util'; import { DataRequestAbortError } from '../../util/data_request'; import { expandToTileBoundaries } from '../es_geo_grid_source/geo_tile_utils'; +import { search } from '../../../../../../../src/plugins/data/public'; export class AbstractESSource extends AbstractVectorSource { constructor(descriptor, inspectorAdapters) { @@ -84,16 +84,22 @@ export class AbstractESSource extends AbstractVectorSource { const abortController = new AbortController(); registerCancelCallback(() => abortController.abort()); + const inspectorRequest = this._inspectorAdapters.requests.start(requestName, { + id: requestId, + description: requestDescription, + }); + let resp; try { - return await fetchSearchSourceAndRecordWithInspector({ - inspectorAdapters: this._inspectorAdapters, - searchSource, - requestName, - requestId, - requestDesc: requestDescription, - abortSignal: abortController.signal, + inspectorRequest.stats(search.getRequestInspectorStats(searchSource)); + searchSource.getSearchRequestBody().then((body) => { + inspectorRequest.json(body); }); + resp = await searchSource.fetch({ abortSignal: abortController.signal }); + inspectorRequest + .stats(search.getResponseInspectorStats(resp, searchSource)) + .ok({ json: resp }); } catch (error) { + inspectorRequest.error({ error }); if (error.name === 'AbortError') { throw new DataRequestAbortError(); } @@ -105,6 +111,8 @@ export class AbstractESSource extends AbstractVectorSource { }) ); } + + return resp; } async makeSearchSource(searchFilters, limit, initialSearchContext) { diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 696c07376575b..4a050cc3d7d19 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -11,9 +11,9 @@ import { ReactElement } from 'react'; import { Adapters } from 'src/plugins/inspector/public'; import { copyPersistentState } from '../../reducers/util'; -import { SourceDescriptor } from '../../../common/descriptor_types'; import { IField } from '../fields/field'; import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; +import { AbstractSourceDescriptor } from '../../../common/descriptor_types'; import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view'; export type SourceEditorArgs = { @@ -56,7 +56,7 @@ export interface ISource { supportsFitToBounds(): Promise; showJoinEditor(): boolean; getJoinsDisabledReason(): string | null; - cloneDescriptor(): SourceDescriptor; + cloneDescriptor(): AbstractSourceDescriptor; getFieldNames(): string[]; getApplyGlobalQuery(): boolean; getIndexPatternIds(): string[]; @@ -70,17 +70,17 @@ export interface ISource { } export class AbstractSource implements ISource { - readonly _descriptor: SourceDescriptor; + readonly _descriptor: AbstractSourceDescriptor; readonly _inspectorAdapters?: Adapters | undefined; - constructor(descriptor: SourceDescriptor, inspectorAdapters?: Adapters) { + constructor(descriptor: AbstractSourceDescriptor, inspectorAdapters?: Adapters) { this._descriptor = descriptor; this._inspectorAdapters = inspectorAdapters; } destroy(): void {} - cloneDescriptor(): SourceDescriptor { + cloneDescriptor(): AbstractSourceDescriptor { return copyPersistentState(this._descriptor); } @@ -133,7 +133,7 @@ export class AbstractSource implements ISource { } getApplyGlobalQuery(): boolean { - return 'applyGlobalQuery' in this._descriptor ? !!this._descriptor.applyGlobalQuery : false; + return !!this._descriptor.applyGlobalQuery; } getIndexPatternIds(): string[] { diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap index 2cf5287ae6594..b005e3ca6b17d 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap @@ -5,7 +5,7 @@ exports[`LayerPanel is rendered 1`] = ` services={ Object { "appName": "maps", - "data": undefined, + "data": Object {}, "storage": Storage { "clear": [Function], "get": [Function], diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js index 33ca80b00c451..1a0eda102986f 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js @@ -40,6 +40,17 @@ jest.mock('./layer_settings', () => ({ }, })); +jest.mock('../../kibana_services', () => { + return { + getData() { + return {}; + }, + getCore() { + return {}; + }, + }; +}); + import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 616d06a5c7b19..43ff274b1353f 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -129,12 +129,12 @@ export class MapEmbeddable extends Embeddable !filter.meta.disabled), query, timeFilters: timeRange, - refresh, + forceRefresh, }) ); } @@ -270,7 +270,7 @@ export class MapEmbeddable extends Embeddable>; -export function getIndexPatternSelectComponent(): any; -export function getHttp(): any; -export function getTimeFilter(): any; -export function getToasts(): any; -export function getIndexPatternService(): IndexPatternsService; -export function getAutocompleteService(): any; -export function getSavedObjectsClient(): any; -export function getMapsCapabilities(): any; -export function getVisualizations(): any; -export function getDocLinks(): any; -export function getCoreChrome(): any; -export function getUiSettings(): any; -export function getIsDarkMode(): boolean; -export function getCoreOverlays(): any; -export function getData(): any; -export function getUiActions(): any; -export function getCore(): any; -export function getNavigation(): any; -export function getCoreI18n(): any; -export function getSearchService(): DataPublicPluginStart['search']; -export function getKibanaCommonConfig(): MapsLegacyConfigType; -export function getMapAppConfig(): MapsConfigType; -export function getIsEmsEnabled(): any; -export function getEmsFontLibraryUrl(): any; -export function getEmsTileLayerId(): any; -export function getEmsFileApiUrl(): any; -export function getEmsTileApiUrl(): any; -export function getEmsLandingPageUrl(): any; -export function getRegionmapLayers(): any; -export function getTilemap(): any; -export function getKibanaVersion(): string; -export function getEnabled(): boolean; -export function getShowMapVisualizationTypes(): boolean; -export function getShowMapsInspectorAdapter(): boolean; -export function getPreserveDrawingBuffer(): boolean; -export function getProxyElasticMapsServiceInMaps(): boolean; -export function getIsGoldPlus(): boolean; -export function fetchSearchSourceAndRecordWithInspector(args: unknown): any; - -export function setLicenseId(args: unknown): void; -export function setInspector(args: unknown): void; -export function setFileUpload(args: unknown): void; -export function setIndexPatternSelect(args: unknown): void; -export function setHttp(args: unknown): void; -export function setTimeFilter(args: unknown): void; -export function setToasts(args: unknown): void; -export function setIndexPatternService(args: unknown): void; -export function setAutocompleteService(args: unknown): void; -export function setSavedObjectsClient(args: unknown): void; -export function setMapsCapabilities(args: unknown): void; -export function setVisualizations(args: unknown): void; -export function setDocLinks(args: unknown): void; -export function setCoreChrome(args: unknown): void; -export function setUiSettings(args: unknown): void; -export function setCoreOverlays(args: unknown): void; -export function setData(args: unknown): void; -export function setUiActions(args: unknown): void; -export function setCore(args: unknown): void; -export function setNavigation(args: unknown): void; -export function setCoreI18n(args: unknown): void; -export function setSearchService(args: DataPublicPluginStart['search']): void; -export function setKibanaCommonConfig(config: MapsLegacyConfigType): void; -export function setMapAppConfig(config: MapsConfigType): void; -export function setKibanaVersion(version: string): void; -export function setIsGoldPlus(isGoldPlus: boolean): void; -export function setEmbeddableService(embeddableService: EmbeddableStart): void; -export function getEmbeddableService(): EmbeddableStart; -export function setNavigateToApp( - navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise -): void; -export const navigateToApp: ( - appId: string, - options?: NavigateToAppOptions | undefined -) => Promise; diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js deleted file mode 100644 index 64aa0e07ffafb..0000000000000 --- a/x-pack/plugins/maps/public/kibana_services.js +++ /dev/null @@ -1,192 +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 { esFilters, search } from '../../../../src/plugins/data/public'; -import _ from 'lodash'; - -export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; -const { getRequestInspectorStats, getResponseInspectorStats } = search; - -let indexPatternService; -export const setIndexPatternService = (dataIndexPatterns) => - (indexPatternService = dataIndexPatterns); -export const getIndexPatternService = () => indexPatternService; - -let autocompleteService; -export const setAutocompleteService = (dataAutoComplete) => - (autocompleteService = dataAutoComplete); -export const getAutocompleteService = () => autocompleteService; - -let licenseId; -export const setLicenseId = (latestLicenseId) => (licenseId = latestLicenseId); -export const getLicenseId = () => { - return licenseId; -}; - -let inspector; -export const setInspector = (newInspector) => (inspector = newInspector); -export const getInspector = () => { - return inspector; -}; - -let fileUploadPlugin; -export const setFileUpload = (fileUpload) => (fileUploadPlugin = fileUpload); -export const getFileUploadComponent = async () => { - return await fileUploadPlugin.getFileUploadComponent(); -}; - -let uiSettings; -export const setUiSettings = (coreUiSettings) => (uiSettings = coreUiSettings); -export const getUiSettings = () => uiSettings; -export const getIsDarkMode = () => { - return getUiSettings().get('theme:darkMode', false); -}; - -let indexPatternSelectComponent; -export const setIndexPatternSelect = (indexPatternSelect) => - (indexPatternSelectComponent = indexPatternSelect); -export const getIndexPatternSelectComponent = () => indexPatternSelectComponent; - -let coreHttp; -export const setHttp = (http) => (coreHttp = http); -export const getHttp = () => coreHttp; - -let dataTimeFilter; -export const setTimeFilter = (timeFilter) => (dataTimeFilter = timeFilter); -export const getTimeFilter = () => dataTimeFilter; - -let toast; -export const setToasts = (notificationToast) => (toast = notificationToast); -export const getToasts = () => toast; - -export async function fetchSearchSourceAndRecordWithInspector({ - searchSource, - requestId, - requestName, - requestDesc, - inspectorAdapters, - abortSignal, -}) { - const inspectorRequest = inspectorAdapters.requests.start(requestName, { - id: requestId, - description: requestDesc, - }); - let resp; - try { - inspectorRequest.stats(getRequestInspectorStats(searchSource)); - searchSource.getSearchRequestBody().then((body) => { - inspectorRequest.json(body); - }); - resp = await searchSource.fetch({ abortSignal }); - inspectorRequest.stats(getResponseInspectorStats(resp, searchSource)).ok({ json: resp }); - } catch (error) { - inspectorRequest.error({ error }); - throw error; - } - - return resp; -} - -let savedObjectsClient; -export const setSavedObjectsClient = (coreSavedObjectsClient) => - (savedObjectsClient = coreSavedObjectsClient); -export const getSavedObjectsClient = () => savedObjectsClient; - -let chrome; -export const setCoreChrome = (coreChrome) => (chrome = coreChrome); -export const getCoreChrome = () => chrome; - -let mapsCapabilities; -export const setMapsCapabilities = (coreAppMapsCapabilities) => - (mapsCapabilities = coreAppMapsCapabilities); -export const getMapsCapabilities = () => mapsCapabilities; - -let visualizations; -export const setVisualizations = (visPlugin) => (visualizations = visPlugin); -export const getVisualizations = () => visualizations; - -let docLinks; -export const setDocLinks = (coreDocLinks) => (docLinks = coreDocLinks); -export const getDocLinks = () => docLinks; - -let overlays; -export const setCoreOverlays = (coreOverlays) => (overlays = coreOverlays); -export const getCoreOverlays = () => overlays; - -let data; -export const setData = (dataPlugin) => (data = dataPlugin); -export const getData = () => data; - -let uiActions; -export const setUiActions = (pluginUiActions) => (uiActions = pluginUiActions); -export const getUiActions = () => uiActions; - -let core; -export const setCore = (kibanaCore) => (core = kibanaCore); -export const getCore = () => core; - -let navigation; -export const setNavigation = (pluginNavigation) => (navigation = pluginNavigation); -export const getNavigation = () => navigation; - -let coreI18n; -export const setCoreI18n = (kibanaCoreI18n) => (coreI18n = kibanaCoreI18n); -export const getCoreI18n = () => coreI18n; - -let dataSearchService; -export const setSearchService = (searchService) => (dataSearchService = searchService); -export const getSearchService = () => dataSearchService; - -let kibanaVersion; -export const setKibanaVersion = (version) => (kibanaVersion = version); -export const getKibanaVersion = () => kibanaVersion; - -// xpack.maps.* kibana.yml settings from this plugin -let mapAppConfig; -export const setMapAppConfig = (config) => (mapAppConfig = config); -export const getMapAppConfig = () => mapAppConfig; - -export const getEnabled = () => getMapAppConfig().enabled; -export const getShowMapVisualizationTypes = () => getMapAppConfig().showMapVisualizationTypes; -export const getShowMapsInspectorAdapter = () => getMapAppConfig().showMapsInspectorAdapter; -export const getPreserveDrawingBuffer = () => getMapAppConfig().preserveDrawingBuffer; - -// map.* kibana.yml settings from maps_legacy plugin that are shared between OSS map visualizations and maps app -let kibanaCommonConfig; -export const setKibanaCommonConfig = (config) => (kibanaCommonConfig = config); -export const getKibanaCommonConfig = () => kibanaCommonConfig; - -export const getIsEmsEnabled = () => getKibanaCommonConfig().includeElasticMapsService; -export const getEmsFontLibraryUrl = () => getKibanaCommonConfig().emsFontLibraryUrl; -export const getEmsTileLayerId = () => getKibanaCommonConfig().emsTileLayerId; -export const getEmsFileApiUrl = () => getKibanaCommonConfig().emsFileApiUrl; -export const getEmsTileApiUrl = () => getKibanaCommonConfig().emsTileApiUrl; -export const getEmsLandingPageUrl = () => getKibanaCommonConfig().emsLandingPageUrl; -export const getProxyElasticMapsServiceInMaps = () => - getKibanaCommonConfig().proxyElasticMapsServiceInMaps; -export const getRegionmapLayers = () => _.get(getKibanaCommonConfig(), 'regionmap.layers', []); -export const getTilemap = () => _.get(getKibanaCommonConfig(), 'tilemap', []); - -let isGoldPlus = false; -export const setIsGoldPlus = (igp) => { - isGoldPlus = igp; -}; - -export const getIsGoldPlus = () => { - return isGoldPlus; -}; - -let embeddableService; -export const setEmbeddableService = (_embeddableService) => { - embeddableService = _embeddableService; -}; -export const getEmbeddableService = () => { - return embeddableService; -}; - -export let navigateToApp; -export function setNavigateToApp(_navigateToApp) { - navigateToApp = _navigateToApp; -} diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts new file mode 100644 index 0000000000000..3b004e2cda67b --- /dev/null +++ b/x-pack/plugins/maps/public/kibana_services.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 _ from 'lodash'; +import { esFilters } from '../../../../src/plugins/data/public'; +import { MapsLegacyConfigType } from '../../../../src/plugins/maps_legacy/public'; +import { MapsConfigType } from '../config'; +import { MapsPluginStartDependencies } from './plugin'; +import { CoreStart } from '../../../../src/core/public'; + +export const SPATIAL_FILTER_TYPE = esFilters.FILTERS.SPATIAL_FILTER; + +let licenseId: string | undefined; +export const setLicenseId = (latestLicenseId: string | undefined) => (licenseId = latestLicenseId); +export const getLicenseId = () => licenseId; +let isGoldPlus: boolean = false; +export const setIsGoldPlus = (igp: boolean) => (isGoldPlus = igp); +export const getIsGoldPlus = () => isGoldPlus; + +let kibanaVersion: string; +export const setKibanaVersion = (version: string) => (kibanaVersion = version); +export const getKibanaVersion = () => kibanaVersion; + +let coreStart: CoreStart; +let pluginsStart: MapsPluginStartDependencies; +export function setStartServices(core: CoreStart, plugins: MapsPluginStartDependencies) { + coreStart = core; + pluginsStart = plugins; +} +export const getIndexPatternService = () => pluginsStart.data.indexPatterns; +export const getAutocompleteService = () => pluginsStart.data.autocomplete; +export const getInspector = () => pluginsStart.inspector; +export const getFileUploadComponent = async () => { + return await pluginsStart.fileUpload.getFileUploadComponent(); +}; +export const getUiSettings = () => coreStart.uiSettings; +export const getIsDarkMode = () => getUiSettings().get('theme:darkMode', false); +export const getIndexPatternSelectComponent = (): any => pluginsStart.data.ui.IndexPatternSelect; +export const getHttp = () => coreStart.http; +export const getTimeFilter = () => pluginsStart.data.query.timefilter.timefilter; +export const getToasts = () => coreStart.notifications.toasts; +export const getSavedObjectsClient = () => coreStart.savedObjects.client; +export const getCoreChrome = () => coreStart.chrome; +export const getMapsCapabilities = () => coreStart.application.capabilities.maps; +export const getDocLinks = () => coreStart.docLinks; +export const getCoreOverlays = () => coreStart.overlays; +export const getData = () => pluginsStart.data; +export const getUiActions = () => pluginsStart.uiActions; +export const getCore = () => coreStart; +export const getNavigation = () => pluginsStart.navigation; +export const getCoreI18n = () => coreStart.i18n; +export const getSearchService = () => pluginsStart.data.search; +export const getEmbeddableService = () => pluginsStart.embeddable; +export const getNavigateToApp = () => coreStart.application.navigateToApp; + +// xpack.maps.* kibana.yml settings from this plugin +let mapAppConfig: MapsConfigType; +export const setMapAppConfig = (config: MapsConfigType) => (mapAppConfig = config); +export const getMapAppConfig = () => mapAppConfig; + +export const getEnabled = () => getMapAppConfig().enabled; +export const getShowMapsInspectorAdapter = () => getMapAppConfig().showMapsInspectorAdapter; +export const getPreserveDrawingBuffer = () => getMapAppConfig().preserveDrawingBuffer; + +// map.* kibana.yml settings from maps_legacy plugin that are shared between OSS map visualizations and maps app +let kibanaCommonConfig: MapsLegacyConfigType; +export const setKibanaCommonConfig = (config: MapsLegacyConfigType) => + (kibanaCommonConfig = config); +export const getKibanaCommonConfig = () => kibanaCommonConfig; + +export const getIsEmsEnabled = () => getKibanaCommonConfig().includeElasticMapsService; +export const getEmsFontLibraryUrl = () => getKibanaCommonConfig().emsFontLibraryUrl; +export const getEmsTileLayerId = () => getKibanaCommonConfig().emsTileLayerId; +export const getEmsFileApiUrl = () => getKibanaCommonConfig().emsFileApiUrl; +export const getEmsTileApiUrl = () => getKibanaCommonConfig().emsTileApiUrl; +export const getEmsLandingPageUrl = () => getKibanaCommonConfig().emsLandingPageUrl; +export const getProxyElasticMapsServiceInMaps = () => + getKibanaCommonConfig().proxyElasticMapsServiceInMaps; +export const getRegionmapLayers = () => _.get(getKibanaCommonConfig(), 'regionmap.layers', []); +export const getTilemap = () => _.get(getKibanaCommonConfig(), 'tilemap', []); diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index b77bf208c5865..5f2a640aa9d0f 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -6,7 +6,7 @@ import { AnyAction } from 'redux'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { IndexPatternsService } from 'src/plugins/data/public/index_patterns'; +import { IndexPatternsContract } from 'src/plugins/data/public/index_patterns'; import { ReactElement } from 'react'; import { IndexPattern } from 'src/plugins/data/public'; import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public'; @@ -29,7 +29,7 @@ interface LazyLoadedMapModules { renderTooltipContent?: RenderToolTipContent, eventHandlers?: EventHandlers ) => Embeddable; - getIndexPatternService: () => IndexPatternsService; + getIndexPatternService: () => IndexPatternsContract; getHttp: () => any; getMapsCapabilities: () => any; createMapStore: () => MapStore; diff --git a/x-pack/plugins/maps/public/maps_vis_type_alias.js b/x-pack/plugins/maps/public/maps_vis_type_alias.js index d90674f0f7725..b7e95cdf987db 100644 --- a/x-pack/plugins/maps/public/maps_vis_type_alias.js +++ b/x-pack/plugins/maps/public/maps_vis_type_alias.js @@ -6,12 +6,10 @@ import { i18n } from '@kbn/i18n'; import { APP_ID, APP_ICON, MAP_PATH } from '../common/constants'; -import { getShowMapVisualizationTypes, getVisualizations } from './kibana_services'; -export function getMapsVisTypeAlias() { - const showMapVisualizationTypes = getShowMapVisualizationTypes(); +export function getMapsVisTypeAlias(visualizations, showMapVisualizationTypes) { if (!showMapVisualizationTypes) { - getVisualizations().hideTypes(['region_map', 'tile_map']); + visualizations.hideTypes(['region_map', 'tile_map']); } const description = i18n.translate('xpack.maps.visTypeAlias.description', { diff --git a/x-pack/plugins/maps/public/meta.ts b/x-pack/plugins/maps/public/meta.ts index 34c5f004fd7f3..5142793bede34 100644 --- a/x-pack/plugins/maps/public/meta.ts +++ b/x-pack/plugins/maps/public/meta.ts @@ -61,7 +61,7 @@ function relativeToAbsolute(url: string): string { } let emsClient: EMSClient | null = null; -let latestLicenseId: string | null = null; +let latestLicenseId: string | undefined; export function getEMSClient(): EMSClient { if (!emsClient) { const proxyElasticMapsServiceInMaps = getProxyElasticMapsServiceInMaps(); @@ -93,7 +93,7 @@ export function getEMSClient(): EMSClient { const licenseId = getLicenseId(); if (latestLicenseId !== licenseId) { latestLicenseId = licenseId; - emsClient.addQueryParams({ license: licenseId }); + emsClient.addQueryParams({ license: licenseId ? licenseId : '' }); } return emsClient; } diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index c374d3cb59b34..e2b40e22bfe7d 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -5,6 +5,9 @@ */ import { Setup as InspectorSetupContract } from 'src/plugins/inspector/public'; +import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { Start as InspectorStartContract } from 'src/plugins/inspector/public'; import { CoreSetup, CoreStart, @@ -15,34 +18,12 @@ import { // @ts-ignore import { MapView } from './inspector/views/map_view'; import { - setAutocompleteService, - setCore, - setCoreChrome, - setCoreI18n, - setCoreOverlays, - setData, - setDocLinks, - setFileUpload, - setHttp, - setIndexPatternSelect, - setIndexPatternService, - setInspector, setIsGoldPlus, setKibanaCommonConfig, setKibanaVersion, setLicenseId, setMapAppConfig, - setMapsCapabilities, - setNavigation, - setSavedObjectsClient, - setSearchService, - setTimeFilter, - setToasts, - setUiActions, - setUiSettings, - setVisualizations, - setEmbeddableService, - setNavigateToApp, + setStartServices, } from './kibana_services'; import { featureCatalogueEntry } from './feature_catalogue_entry'; // @ts-ignore @@ -58,66 +39,29 @@ import { ILicense } from '../../licensing/common/types'; import { lazyLoadMapModules } from './lazy_load_bundle'; import { MapsStartApi } from './api'; import { createSecurityLayerDescriptors, registerLayerWizard, registerSource } from './api'; +import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { MapsLegacyConfigType } from '../../../../src/plugins/maps_legacy/public'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { LicensingPluginStart } from '../../licensing/public'; +import { StartContract as FileUploadStartContract } from '../../file_upload/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; home: HomePublicPluginSetup; visualizations: VisualizationsSetup; embeddable: EmbeddableSetup; - mapsLegacy: { config: unknown }; + mapsLegacy: { config: MapsLegacyConfigType }; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MapsPluginStartDependencies {} -export const bindSetupCoreAndPlugins = ( - core: CoreSetup, - plugins: any, - config: MapsConfigType, - kibanaVersion: string -) => { - const { licensing, mapsLegacy } = plugins; - const { uiSettings, http, notifications } = core; - if (licensing) { - licensing.license$.subscribe(({ uid }: { uid: string }) => setLicenseId(uid)); - } - setHttp(http); - setToasts(notifications.toasts); - setVisualizations(plugins.visualizations); - setUiSettings(uiSettings); - setKibanaCommonConfig(mapsLegacy.config); - setMapAppConfig(config); - setKibanaVersion(kibanaVersion); -}; - -export const bindStartCoreAndPlugins = (core: CoreStart, plugins: any) => { - const { fileUpload, data, inspector, licensing } = plugins; - if (licensing) { - licensing.license$.subscribe((license: ILicense) => { - const gold = license.check(APP_ID, 'gold'); - setIsGoldPlus(gold.state === 'valid'); - }); - } - - setInspector(inspector); - setFileUpload(fileUpload); - setIndexPatternSelect(data.ui.IndexPatternSelect); - setTimeFilter(data.query.timefilter.timefilter); - setSearchService(data.search); - setIndexPatternService(data.indexPatterns); - setAutocompleteService(data.autocomplete); - setCore(core); - setSavedObjectsClient(core.savedObjects.client); - setCoreChrome(core.chrome); - setCoreOverlays(core.overlays); - setMapsCapabilities(core.application.capabilities.maps); - setDocLinks(core.docLinks); - setData(plugins.data); - setUiActions(plugins.uiActions); - setNavigation(plugins.navigation); - setCoreI18n(core.i18n); - setEmbeddableService(plugins.embeddable); - setNavigateToApp(core.application.navigateToApp); -}; +export interface MapsPluginStartDependencies { + data: DataPublicPluginStart; + embeddable: EmbeddableStart; + fileUpload: FileUploadStartContract; + inspector: InspectorStartContract; + licensing: LicensingPluginStart; + navigation: NavigationPublicPluginStart; + uiActions: UiActionsStart; +} /** * These are the interfaces with your public contracts. You should export these @@ -144,14 +88,16 @@ export class MapsPlugin public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies) { const config = this._initializerContext.config.get(); - const kibanaVersion = this._initializerContext.env.packageInfo.version; - const { inspector, home, visualizations, embeddable } = plugins; - bindSetupCoreAndPlugins(core, plugins, config, kibanaVersion); + setKibanaCommonConfig(plugins.mapsLegacy.config); + setMapAppConfig(config); + setKibanaVersion(this._initializerContext.env.packageInfo.version); - inspector.registerView(MapView); - home.featureCatalogue.register(featureCatalogueEntry); - visualizations.registerAlias(getMapsVisTypeAlias()); - embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); + plugins.inspector.registerView(MapView); + plugins.home.featureCatalogue.register(featureCatalogueEntry); + plugins.visualizations.registerAlias( + getMapsVisTypeAlias(plugins.visualizations, config.showMapVisualizationTypes) + ); + plugins.embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); core.application.register({ id: APP_ID, @@ -162,16 +108,23 @@ export class MapsPlugin category: DEFAULT_APP_CATEGORIES.kibana, // @ts-expect-error async mount(context, params) { - const [coreStart, startPlugins] = await core.getStartServices(); - bindStartCoreAndPlugins(coreStart, startPlugins); const { renderApp } = await lazyLoadMapModules(); return renderApp(context, params); }, }); } - public start(core: CoreStart, plugins: any): MapsStartApi { - bindStartCoreAndPlugins(core, plugins); + public start(core: CoreStart, plugins: MapsPluginStartDependencies): MapsStartApi { + if (plugins.licensing) { + plugins.licensing.license$.subscribe((license: ILicense) => { + const gold = license.check(APP_ID, 'gold'); + setIsGoldPlus(gold.state === 'valid'); + setLicenseId(license.uid); + }); + } + + setStartServices(core, plugins); + return { createSecurityLayerDescriptors, registerLayerWizard, diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js index 15a036a0a1d7c..326db7289e60d 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.js @@ -52,13 +52,13 @@ function mapStateToProps(state = {}) { function mapDispatchToProps(dispatch) { return { - dispatchSetQuery: ({ refresh, filters, query, timeFilters }) => { + dispatchSetQuery: ({ forceRefresh, filters, query, timeFilters }) => { dispatch( setQuery({ filters, query, timeFilters, - refresh, + forceRefresh, }) ); }, diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js index 23625b4591db7..58f0bf16e93f2 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js @@ -142,7 +142,7 @@ export class MapsAppView extends React.Component { return; } - this._onQueryChange({ time: globalState.time, refresh: true }); + this._onQueryChange({ time: globalState.time }); }; async _updateIndexPatterns() { @@ -160,7 +160,7 @@ export class MapsAppView extends React.Component { } } - _onQueryChange = ({ filters, query, time, refresh = false }) => { + _onQueryChange = ({ filters, query, time, forceRefresh = false }) => { const { filterManager } = getData().query; if (filters) { @@ -168,7 +168,7 @@ export class MapsAppView extends React.Component { } this.props.dispatchSetQuery({ - refresh, + forceRefresh, filters: filterManager.getFilters(), query, timeFilters: time, @@ -336,7 +336,7 @@ export class MapsAppView extends React.Component { this._onQueryChange({ query, time: dateRange, - refresh: true, + forceRefresh: true, }); }} onFiltersUpdated={this._onFiltersChange} diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx index 8a4d8ae555895..35d8490f1a886 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx @@ -13,7 +13,7 @@ import { getInspector, getToasts, getCoreI18n, - navigateToApp, + getNavigateToApp, } from '../../../kibana_services'; import { SavedObjectSaveModalOrigin, @@ -117,7 +117,7 @@ export function getTopNavConfig({ state: { id, type: MAP_SAVED_OBJECT_TYPE }, }); } else { - navigateToApp(originatingApp); + getNavigateToApp()(originatingApp); } } diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index f0286d7e5811f..8688bbe549f51 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -23,7 +23,6 @@ import { ESGeoGridSourceDescriptor, ESSearchSourceDescriptor, LayerDescriptor, - SourceDescriptor, } from '../../common/descriptor_types'; import { MapSavedObject } from '../../common/map_saved_object_type'; // @ts-ignore @@ -154,7 +153,7 @@ function isGeoShapeAggLayer(indexPatterns: IIndexPattern[], layer: LayerDescript return false; } - const sourceDescriptor: SourceDescriptor = layer.sourceDescriptor; + const sourceDescriptor = layer.sourceDescriptor; if (sourceDescriptor.type === SOURCE_TYPES.ES_GEO_GRID) { return isFieldGeoShape( indexPatterns, diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js index 4dc4201e358fb..5173ea89c5dd1 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js @@ -25,6 +25,7 @@ import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { AlertsStatus } from '../../../alerts/status'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../../common/constants'; +import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; import './listing.scss'; const IsClusterSupported = ({ isSupported, children }) => { @@ -78,7 +79,7 @@ const getColumns = ( if (cluster.isSupported) { return ( changeCluster(cluster.cluster_uuid, cluster.ccs)} + href={getSafeForExternalLink(`#/overview?_g=(cluster_uuid:${cluster.cluster_uuid})`)} data-test-subj="clusterLink" > {value} diff --git a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx index 14cdc26c80f53..43a03bb771501 100644 --- a/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/test_utilities/simulator/index.tsx @@ -175,14 +175,24 @@ export class Simulator { } /** - * Return an Enzyme ReactWrapper for any child elements of a specific processNodeElement - * - * @param entityID The entity ID of the proocess node to select in - * @param selector The selector for the child element of the process node + * The button that opens a node's submenu. */ - public processNodeChildElements(entityID: string, selector: string): ReactWrapper { + public processNodeSubmenuButton( + /** nodeID for the related node */ entityID: string + ): ReactWrapper { return this.domNodes( - `${processNodeElementSelector({ entityID })} [data-test-subj="${selector}"]` + `[data-test-subj="resolver:submenu:button"][data-test-resolver-node-id="${entityID}"]` + ); + } + + /** + * The primary button (used to select a node) which contains a label for the node as its content. + */ + public processNodePrimaryButton( + /** nodeID for the related node */ entityID: string + ): ReactWrapper { + return this.domNodes( + `[data-test-subj="resolver:node:primary-button"][data-test-resolver-node-id="${entityID}"]` ); } diff --git a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx index 09fcd273a9c9b..3265ee8bcfca0 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/clickthrough.test.tsx @@ -62,13 +62,13 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', selectedOriginCount: simulator.selectedProcessNode(entityIDs.origin).length, unselectedFirstChildCount: simulator.unselectedProcessNode(entityIDs.firstChild).length, unselectedSecondChildCount: simulator.unselectedProcessNode(entityIDs.secondChild).length, - processNodeCount: simulator.processNodeElements().length, + nodePrimaryButtonCount: simulator.testSubject('resolver:node:primary-button').length, })) ).toYieldEqualTo({ selectedOriginCount: 1, unselectedFirstChildCount: 1, unselectedSecondChildCount: 1, - processNodeCount: 3, + nodePrimaryButtonCount: 3, }); }); @@ -82,13 +82,14 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', }); describe("when the second child node's first button has been clicked", () => { - beforeEach(() => { - // Click the first button under the second child element. - simulator - .processNodeElements({ entityID: entityIDs.secondChild }) - .find('button') - .first() - .simulate('click'); + beforeEach(async () => { + const button = await simulator.resolveWrapper(() => + simulator.processNodePrimaryButton(entityIDs.secondChild) + ); + // Click the second child node's primary button + if (button) { + button.simulate('click'); + } }); it('should render the second child node as selected, and the origin as not selected, and the query string should indicate that the second child is selected', async () => { await expect( @@ -141,23 +142,20 @@ describe('Resolver, when analyzing a tree that has two related events for the or graphElements: simulator.testSubject('resolver:graph').length, graphLoadingElements: simulator.testSubject('resolver:graph:loading').length, graphErrorElements: simulator.testSubject('resolver:graph:error').length, - originNode: simulator.processNodeElements({ entityID: entityIDs.origin }).length, + originNodeButton: simulator.processNodePrimaryButton(entityIDs.origin).length, })) ).toYieldEqualTo({ graphElements: 1, graphLoadingElements: 0, graphErrorElements: 0, - originNode: 1, + originNodeButton: 1, }); }); it('should render a related events button', async () => { await expect( simulator.map(() => ({ - relatedEventButtons: simulator.processNodeChildElements( - entityIDs.origin, - 'resolver:submenu:button' - ).length, + relatedEventButtons: simulator.processNodeSubmenuButton(entityIDs.origin).length, })) ).toYieldEqualTo({ relatedEventButtons: 1, @@ -166,7 +164,7 @@ describe('Resolver, when analyzing a tree that has two related events for the or describe('when the related events button is clicked', () => { beforeEach(async () => { const button = await simulator.resolveWrapper(() => - simulator.processNodeChildElements(entityIDs.origin, 'resolver:submenu:button') + simulator.processNodeSubmenuButton(entityIDs.origin) ); if (button) { button.simulate('click'); @@ -183,7 +181,7 @@ describe('Resolver, when analyzing a tree that has two related events for the or describe('and when the related events button is clicked again', () => { beforeEach(async () => { const button = await simulator.resolveWrapper(() => - simulator.processNodeChildElements(entityIDs.origin, 'resolver:submenu:button') + simulator.processNodeSubmenuButton(entityIDs.origin) ); if (button) { button.simulate('click'); diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 2a5d91028d9f5..2bb104801866f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -404,6 +404,8 @@ const UnstyledProcessEventDot = React.memo( }} tabIndex={-1} title={eventModel.processNameSafeVersion(event)} + data-test-subj="resolver:node:primary-button" + data-test-resolver-node-id={nodeID} > @@ -433,6 +435,7 @@ const UnstyledProcessEventDot = React.memo( menuTitle={subMenuAssets.relatedEvents.title} projectionMatrix={projectionMatrix} optionsWithActions={relatedEventStatusOrOptions} + nodeID={nodeID} /> )} diff --git a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts index 26c25cfab2c21..a86237e0e2b45 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/query_params.test.ts @@ -34,12 +34,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', describe("when the second child node's first button has been clicked", () => { beforeEach(async () => { - const node = await simulator.resolveWrapper(() => - simulator.processNodeElements({ entityID: entityIDs.secondChild }).find('button') + const button = await simulator.resolveWrapper(() => + simulator.processNodePrimaryButton(entityIDs.secondChild) ); - if (node) { + if (button) { // Click the first button under the second child element. - node.first().simulate('click'); + button.simulate('click'); } }); const expectedSearch = urlSearch(resolverComponentInstanceID, { @@ -68,12 +68,12 @@ describe('Resolver, when analyzing a tree that has no ancestors and 2 children', }); describe("when the user clicks the second child node's button again", () => { beforeEach(async () => { - const node = await simulator.resolveWrapper(() => - simulator.processNodeElements({ entityID: entityIDs.secondChild }).find('button') + const button = await simulator.resolveWrapper(() => + simulator.processNodePrimaryButton(entityIDs.secondChild) ); - if (node) { + if (button) { // Click the first button under the second child element. - node.first().simulate('click'); + button.simulate('click'); } }); it(`should have a url search of ${urlSearch(newInstanceID, { diff --git a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx index 359a4e2dafd2e..14d6470c95207 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/submenu.tsx @@ -137,6 +137,7 @@ const NodeSubMenuComponents = React.memo( optionsWithActions, className, projectionMatrix, + nodeID, }: { menuTitle: string; className?: string; @@ -148,6 +149,7 @@ const NodeSubMenuComponents = React.memo( * Receive the projection matrix, so we can see when the camera position changed, so we can force the submenu to reposition itself. */ projectionMatrix: Matrix3; + nodeID: string; } & { optionsWithActions?: ResolverSubmenuOptionList | string | undefined; }) => { @@ -236,6 +238,7 @@ const NodeSubMenuComponents = React.memo( iconSide="right" tabIndex={-1} data-test-subj="resolver:submenu:button" + data-test-resolver-node-id={nodeID} > {count ? : ''} {menuTitle} diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx index d3139601bfa17..365444032b402 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx @@ -55,7 +55,8 @@ jest.mock('react-virtualized-auto-sizer', () => { }) => children({ width: 100, height: 500 }); }); -describe('OpenTimelineModal', () => { +// Failing: See https://github.com/elastic/kibana/issues/74814 +describe.skip('OpenTimelineModal', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); const mockInstallPrepackagedTimelines = jest.fn(); beforeEach(() => { diff --git a/x-pack/plugins/task_manager/server/buffered_task_store.test.ts b/x-pack/plugins/task_manager/server/buffered_task_store.test.ts index 8e18405c79ed2..8ddb9f81c2a8f 100644 --- a/x-pack/plugins/task_manager/server/buffered_task_store.test.ts +++ b/x-pack/plugins/task_manager/server/buffered_task_store.test.ts @@ -58,6 +58,39 @@ describe('Buffered Task Store', () => { ); expect(await results[2]).toMatchObject(tasks[2]); }); + + test('handles multiple items with the same id', async () => { + const taskStore = taskStoreMock.create({ maxAttempts: 10 }); + const bufferedStore = new BufferedTaskStore(taskStore, {}); + + const duplicateIdTask = mockTask(); + const tasks = [ + duplicateIdTask, + mockTask(), + mockTask(), + { ...mockTask(), id: duplicateIdTask.id }, + ]; + + taskStore.bulkUpdate.mockResolvedValueOnce([ + asOk(tasks[0]), + asErr({ entity: tasks[1], error: new Error('Oh no, something went terribly wrong') }), + asOk(tasks[2]), + asOk(tasks[3]), + ]); + + const results = [ + bufferedStore.update(tasks[0]), + bufferedStore.update(tasks[1]), + bufferedStore.update(tasks[2]), + bufferedStore.update(tasks[3]), + ]; + expect(await results[0]).toMatchObject(tasks[0]); + expect(results[1]).rejects.toMatchInlineSnapshot( + `[Error: Oh no, something went terribly wrong]` + ); + expect(await results[2]).toMatchObject(tasks[2]); + expect(await results[3]).toMatchObject(tasks[3]); + }); }); }); diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts index f32a755515a95..25abd92b32a26 100644 --- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts +++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts @@ -6,6 +6,7 @@ import { createBuffer, Entity, OperationError, BulkOperation } from './bulk_operation_buffer'; import { mapErr, asOk, asErr, Ok, Err } from './result_type'; +import { mockLogger } from '../test_utils'; interface TaskInstance extends Entity { attempts: number; @@ -227,5 +228,38 @@ describe('Bulk Operation Buffer', () => { done(); }); }); + + test('logs unknown bulk operation results', async (done) => { + const bulkUpdate: jest.Mocked> = jest.fn( + ([task1, task2, task3]) => { + return Promise.resolve([ + incrementAttempts(task1), + errorAttempts(createTask()), + incrementAttempts(createTask()), + ]); + } + ); + + const logger = mockLogger(); + + const bufferedUpdate = createBuffer(bulkUpdate, { logger }); + + const task1 = createTask(); + const task2 = createTask(); + const task3 = createTask(); + + return Promise.all([ + expect(bufferedUpdate(task1)).resolves.toMatchObject(incrementAttempts(task1)), + expect(bufferedUpdate(task2)).rejects.toMatchObject( + asErr(new Error(`Unhandled buffered operation for entity: ${task2.id}`)) + ), + expect(bufferedUpdate(task3)).rejects.toMatchObject( + asErr(new Error(`Unhandled buffered operation for entity: ${task3.id}`)) + ), + ]).then(() => { + expect(logger.warn).toHaveBeenCalledTimes(2); + done(); + }); + }); }); }); diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts index c8e5b837fa36c..57a14c2f8a56b 100644 --- a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts +++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { keyBy, map } from 'lodash'; +import { map } from 'lodash'; import { Subject, race, from } from 'rxjs'; import { bufferWhen, filter, bufferCount, flatMap, mapTo, first } from 'rxjs/operators'; import { either, Result, asOk, asErr, Ok, Err } from './result_type'; +import { Logger } from '../types'; export interface BufferOptions { bufferMaxDuration?: number; bufferMaxOperations?: number; + logger?: Logger; } export interface Entity { @@ -41,14 +43,14 @@ const FLUSH = true; export function createBuffer( bulkOperation: BulkOperation, - { bufferMaxDuration = 0, bufferMaxOperations = Number.MAX_VALUE }: BufferOptions = {} + { bufferMaxDuration = 0, bufferMaxOperations = Number.MAX_VALUE, logger }: BufferOptions = {} ): Operation { const flushBuffer = new Subject(); const storeUpdateBuffer = new Subject<{ entity: Input; onSuccess: (entity: Ok) => void; - onFailure: (error: Err) => void; + onFailure: (error: Err) => void; }>(); storeUpdateBuffer @@ -56,24 +58,61 @@ export function createBuffer flushBuffer), filter((tasks) => tasks.length > 0) ) - .subscribe((entities) => { - const entityById = keyBy(entities, ({ entity: { id } }) => id); - bulkOperation(map(entities, 'entity')) + .subscribe((bufferedEntities) => { + bulkOperation(map(bufferedEntities, 'entity')) .then((results) => { results.forEach((result) => either( result, (entity) => { - entityById[entity.id].onSuccess(asOk(entity)); + either( + pullFirstWhere(bufferedEntities, ({ entity: { id } }) => id === entity.id), + ({ onSuccess }) => { + onSuccess(asOk(entity)); + }, + () => { + if (logger) { + logger.warn( + `Unhandled successful Bulk Operation result: ${ + entity?.id ? entity.id : entity + }` + ); + } + } + ); }, ({ entity, error }: OperationError) => { - entityById[entity.id].onFailure(asErr(error)); + either( + pullFirstWhere(bufferedEntities, ({ entity: { id } }) => id === entity.id), + ({ onFailure }) => { + onFailure(asErr(error)); + }, + () => { + if (logger) { + logger.warn( + `Unhandled failed Bulk Operation result: ${entity?.id ? entity.id : entity}` + ); + } + } + ); } ) ); + + // if any `bufferedEntities` remain in the array then there was no result we could map to them in the bulkOperation + // call their failure handler to avoid hanging the promise returned to the call site + bufferedEntities.forEach((unhandledBufferedEntity) => { + unhandledBufferedEntity.onFailure( + asErr( + new Error( + `Unhandled buffered operation for entity: ${unhandledBufferedEntity.entity.id}` + ) + ) + ); + }); }) .catch((ex) => { - entities.forEach(({ onFailure }) => onFailure(asErr(ex))); + bufferedEntities.forEach(({ onFailure }) => onFailure(asErr(ex))); }); }); @@ -120,3 +159,10 @@ function resolveIn(ms: number) { setTimeout(resolve, ms); }); } + +function pullFirstWhere(collection: T[], predicate: (entity: T) => boolean): Result { + const indexOfFirstEntity = collection.findIndex(predicate); + return indexOfFirstEntity >= 0 + ? asOk(collection.splice(indexOfFirstEntity, 1)[0]) + : asErr(undefined); +} diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index 7165fd28678c1..9c194b3fb9dd2 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -146,6 +146,7 @@ export class TaskManager { this.bufferedStore = new BufferedTaskStore(this.store, { bufferMaxOperations: opts.config.max_workers, + logger: this.logger, }); this.pool = new TaskPool({ @@ -283,7 +284,7 @@ export class TaskManager { */ public async schedule( taskInstance: TaskInstanceWithDeprecatedFields, - options?: object + options?: Record ): Promise { await this.waitUntilStarted(); const { taskInstance: modifiedTask } = await this.middleware.beforeSave({ @@ -318,7 +319,7 @@ export class TaskManager { */ public async ensureScheduled( taskInstance: TaskInstanceWithId, - options?: object + options?: Record ): Promise { try { return await this.schedule(taskInstance, options); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx index 6769c8bab0732..7e4fe1de8be8d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/action_wizard.tsx @@ -14,13 +14,21 @@ import { EuiSpacer, EuiText, EuiToolTip, + EuiFormFieldset, + EuiCheckableCard, + EuiTextColor, + EuiTitle, + EuiLink, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { txtChangeButton } from './i18n'; +import { txtChangeButton, txtTriggerPickerHelpText, txtTriggerPickerLabel } from './i18n'; import './action_wizard.scss'; -import { ActionFactory } from '../../dynamic_actions'; +import { ActionFactory, BaseActionFactoryContext } from '../../dynamic_actions'; +import { Trigger, TriggerId } from '../../../../../../src/plugins/ui_actions/public'; -export interface ActionWizardProps { +export interface ActionWizardProps< + ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext +> { /** * List of available action factories */ @@ -51,7 +59,22 @@ export interface ActionWizardProps { /** * Context will be passed into ActionFactory's methods */ - context: object; + context: ActionFactoryContext; + + /** + * Trigger selection has changed + * @param triggers + */ + onSelectedTriggersChange: (triggers?: TriggerId[]) => void; + + getTriggerInfo: (triggerId: TriggerId) => Trigger; + + /** + * List of possible triggers in current context + */ + supportedTriggers: TriggerId[]; + + triggerPickerDocsLink?: string; } export const ActionWizard: React.FC = ({ @@ -61,6 +84,10 @@ export const ActionWizard: React.FC = ({ onConfigChange, config, context, + onSelectedTriggersChange, + getTriggerInfo, + supportedTriggers, + triggerPickerDocsLink, }) => { // auto pick action factory if there is only 1 available if ( @@ -71,7 +98,16 @@ export const ActionWizard: React.FC = ({ onActionFactoryChange(actionFactories[0]); } + // auto pick selected trigger if none is picked + if (currentActionFactory && !((context.triggers?.length ?? 0) > 0)) { + const triggers = getTriggersForActionFactory(currentActionFactory, supportedTriggers); + if (triggers.length > 0) { + onSelectedTriggersChange([triggers[0]]); + } + } + if (currentActionFactory && config) { + const allTriggers = getTriggersForActionFactory(currentActionFactory, supportedTriggers); return ( = ({ onConfigChange={(newConfig) => { onConfigChange(newConfig); }} + allTriggers={allTriggers} + getTriggerInfo={getTriggerInfo} + onSelectedTriggersChange={onSelectedTriggersChange} + triggerPickerDocsLink={triggerPickerDocsLink} /> ); } @@ -99,13 +139,84 @@ export const ActionWizard: React.FC = ({ ); }; -interface SelectedActionFactoryProps { +interface TriggerPickerProps { + triggers: TriggerId[]; + selectedTriggers?: TriggerId[]; + getTriggerInfo: (triggerId: TriggerId) => Trigger; + onSelectedTriggersChange: (triggers?: TriggerId[]) => void; + triggerPickerDocsLink?: string; +} + +const TriggerPicker: React.FC = ({ + triggers, + selectedTriggers, + getTriggerInfo, + onSelectedTriggersChange, + triggerPickerDocsLink, +}) => { + const selectedTrigger = selectedTriggers ? selectedTriggers[0] : undefined; + return ( + +
+ {txtTriggerPickerLabel}{' '} + + {txtTriggerPickerHelpText} + +
+ + ), + }} + style={{ maxWidth: `80%` }} + > + {triggers.map((trigger) => ( + + + + {getTriggerInfo(trigger)?.title ?? 'Unknown'} + + {getTriggerInfo(trigger)?.description && ( +
+ + + {getTriggerInfo(trigger)?.description} + + +
+ )} + + } + name={trigger} + value={trigger} + checked={selectedTrigger === trigger} + onChange={() => onSelectedTriggersChange([trigger])} + data-test-subj={`triggerPicker-${trigger}`} + /> + +
+ ))} +
+ ); +}; + +interface SelectedActionFactoryProps< + ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext +> { actionFactory: ActionFactory; config: object; - context: object; + context: ActionFactoryContext; onConfigChange: (config: object) => void; showDeselect: boolean; onDeselect: () => void; + allTriggers: TriggerId[]; + getTriggerInfo: (triggerId: TriggerId) => Trigger; + onSelectedTriggersChange: (triggers?: TriggerId[]) => void; + triggerPickerDocsLink?: string; } export const TEST_SUBJ_SELECTED_ACTION_FACTORY = 'selectedActionFactory'; @@ -117,6 +228,10 @@ const SelectedActionFactory: React.FC = ({ onConfigChange, config, context, + allTriggers, + getTriggerInfo, + onSelectedTriggersChange, + triggerPickerDocsLink, }) => { return (
= ({ )} - + {allTriggers.length > 1 && ( + <> + + + + )} +
= ({ ); }; -interface ActionFactorySelectorProps { +interface ActionFactorySelectorProps< + ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext +> { actionFactories: ActionFactory[]; - context: object; + context: ActionFactoryContext; onActionFactorySelected: (actionFactory: ActionFactory) => void; } @@ -224,3 +353,10 @@ const ActionFactorySelector: React.FC = ({ ); }; + +function getTriggersForActionFactory( + actionFactory: ActionFactory, + allTriggers: TriggerId[] +): TriggerId[] { + return actionFactory.supportedTriggers().filter((trigger) => allTriggers.includes(trigger)); +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts index 3e7e211dc7738..678457f9794f3 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/i18n.ts @@ -12,3 +12,17 @@ export const txtChangeButton = i18n.translate( defaultMessage: 'Change', } ); + +export const txtTriggerPickerLabel = i18n.translate( + 'xpack.uiActionsEnhanced.components.actionWizard.triggerPickerLabel', + { + defaultMessage: 'Pick a trigger:', + } +); + +export const txtTriggerPickerHelpText = i18n.translate( + 'xpack.uiActionsEnhanced.components.actionWizard.helpText', + { + defaultMessage: "What's this?", + } +); diff --git a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx index 2672a086dca73..2ac8dd6392552 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/components/action_wizard/test_data.tsx @@ -8,9 +8,16 @@ import React, { useState } from 'react'; import { EuiFieldText, EuiFormRow, EuiSelect, EuiSwitch } from '@elastic/eui'; import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; import { ActionWizard } from './action_wizard'; -import { ActionFactoryDefinition, ActionFactory } from '../../dynamic_actions'; +import { ActionFactory, ActionFactoryDefinition } from '../../dynamic_actions'; import { CollectConfigProps } from '../../../../../../src/plugins/kibana_utils/public'; import { licenseMock } from '../../../../licensing/common/licensing.mock'; +import { + APPLY_FILTER_TRIGGER, + SELECT_RANGE_TRIGGER, + Trigger, + TriggerId, + VALUE_CLICK_TRIGGER, +} from '../../../../../../src/plugins/ui_actions/public'; type ActionBaseConfig = object; @@ -104,6 +111,9 @@ export const dashboardDrilldownActionFactory: ActionFactoryDefinition< execute: async () => alert('Navigate to dashboard!'), enhancements: {}, }), + supportedTriggers(): any[] { + return [APPLY_FILTER_TRIGGER]; + }, }; export const dashboardFactory = new ActionFactory(dashboardDrilldownActionFactory, () => @@ -161,16 +171,45 @@ export const urlDrilldownActionFactory: ActionFactoryDefinition null as any, + supportedTriggers(): any[] { + return [VALUE_CLICK_TRIGGER, SELECT_RANGE_TRIGGER]; + }, }; export const urlFactory = new ActionFactory(urlDrilldownActionFactory, () => licenseMock.createLicense() ); +export const mockSupportedTriggers: TriggerId[] = [ + VALUE_CLICK_TRIGGER, + SELECT_RANGE_TRIGGER, + APPLY_FILTER_TRIGGER, +]; +export const mockGetTriggerInfo = (triggerId: TriggerId): Trigger => { + const titleMap = { + [VALUE_CLICK_TRIGGER]: 'Single click', + [SELECT_RANGE_TRIGGER]: 'Range selection', + [APPLY_FILTER_TRIGGER]: 'Apply filter', + } as Record; + + const descriptionMap = { + [VALUE_CLICK_TRIGGER]: 'A single point clicked on a visualization', + [SELECT_RANGE_TRIGGER]: 'Select a group of values', + [APPLY_FILTER_TRIGGER]: 'Apply filter description...', + } as Record; + + return { + id: triggerId, + title: titleMap[triggerId] ?? 'Unknown', + description: descriptionMap[triggerId] ?? 'Unknown description', + }; +}; + export function Demo({ actionFactories }: { actionFactories: Array> }) { const [state, setState] = useState<{ currentActionFactory?: ActionFactory; config?: ActionBaseConfig; + selectedTriggers?: TriggerId[]; }>({}); function changeActionFactory(newActionFactory?: ActionFactory) { @@ -200,7 +239,15 @@ export function Demo({ actionFactories }: { actionFactories: Array { + setState({ + ...state, + selectedTriggers: triggers, + }); + }} + getTriggerInfo={mockGetTriggerInfo} + supportedTriggers={[VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, SELECT_RANGE_TRIGGER]} />

@@ -210,6 +257,7 @@ export function Demo({ actionFactories }: { actionFactories: Array +
Picked trigger: {state.selectedTriggers?.[0]}
); } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx index 0b0339a625c50..f7284539ab2fe 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx @@ -25,10 +25,25 @@ const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ alert(JSON.stringify(args)); }, } as any, + getTrigger: (triggerId) => ({ + id: triggerId, + }), }); -storiesOf('components/FlyoutManageDrilldowns', module).add('default', () => ( - {}}> - - -)); +storiesOf('components/FlyoutManageDrilldowns', module) + .add('default (3 triggers)', () => ( + {}}> + + + )) + .add('Only filter is supported', () => ( + {}}> + + + )); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index e98701a05ce89..2412cdd51748c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -7,7 +7,12 @@ import React from 'react'; import { cleanup, fireEvent, render, wait } from '@testing-library/react/pure'; import { createFlyoutManageDrilldowns } from './connected_flyout_manage_drilldowns'; -import { dashboardFactory, urlFactory } from '../../../components/action_wizard/test_data'; +import { + dashboardFactory, + mockGetTriggerInfo, + mockSupportedTriggers, + urlFactory, +} from '../../../components/action_wizard/test_data'; import { StubBrowserStorage } from '../../../../../../../src/test_utils/public/stub_browser_storage'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; import { mockDynamicActionManager } from './test_data'; @@ -24,6 +29,7 @@ const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ actionFactories: [dashboardFactory as ActionFactory, urlFactory as ActionFactory], storage: new Storage(new StubBrowserStorage()), toastService: toasts, + getTrigger: mockGetTriggerInfo, }); // https://github.com/elastic/kibana/issues/59469 @@ -31,12 +37,18 @@ afterEach(cleanup); beforeEach(() => { storage.clear(); + mockDynamicActionManager.state.set({ ...mockDynamicActionManager.state.get(), events: [] }); (toasts as jest.Mocked).addSuccess.mockClear(); (toasts as jest.Mocked).addError.mockClear(); }); test('Allows to manage drilldowns', async () => { - const screen = render(); + const screen = render( + + ); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); @@ -103,7 +115,12 @@ test('Allows to manage drilldowns', async () => { }); test('Can delete multiple drilldowns', async () => { - const screen = render(); + const screen = render( + + ); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); @@ -143,6 +160,7 @@ test('Create only mode', async () => { dynamicActionManager={mockDynamicActionManager} viewMode={'create'} onClose={onClose} + supportedTriggers={mockSupportedTriggers} /> ); // wait for initial render. It is async because resolving compatible action factories is async @@ -163,7 +181,11 @@ test('Create only mode', async () => { test('After switching between action factories state is restored', async () => { const screen = render( - + ); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); @@ -200,7 +222,12 @@ test("Error when can't save drilldown changes", async () => { jest.spyOn(mockDynamicActionManager, 'createEvent').mockImplementationOnce(async () => { throw error; }); - const screen = render(); + const screen = render( + + ); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); fireEvent.click(screen.getByText(/Create new/i)); @@ -218,7 +245,12 @@ test("Error when can't save drilldown changes", async () => { }); test('Should show drilldown welcome message. Should be able to dismiss it', async () => { - let screen = render(); + let screen = render( + + ); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); @@ -228,8 +260,63 @@ test('Should show drilldown welcome message. Should be able to dismiss it', asyn expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull(); cleanup(); - screen = render(); + screen = render( + + ); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull(); }); + +test('Drilldown type is not shown if no supported trigger', async () => { + const screen = render( + + ); + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + expect(screen.queryByText(/Go to Dashboard/i)).not.toBeInTheDocument(); // dashboard action is not visible, because APPLY_FILTER_TRIGGER not supported + expect(screen.getByTestId('selectedActionFactory-Url')).toBeInTheDocument(); +}); + +test('Can pick a trigger', async () => { + const screen = render( + + ); + // wait for initial render. It is async because resolving compatible action factories is async + await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); + + // input drilldown name + const name = 'Test name'; + fireEvent.change(screen.getByLabelText(/name/i), { + target: { value: name }, + }); + + // select URL one + fireEvent.click(screen.getByText(/Go to URL/i)); + + // Input url + const URL = 'https://elastic.co'; + fireEvent.change(screen.getByLabelText(/url/i), { + target: { value: URL }, + }); + + fireEvent.click(screen.getByTestId('triggerPicker-SELECT_RANGE_TRIGGER').querySelector('input')!); + + const [, createButton] = screen.getAllByText(/Create Drilldown/i); + + expect(createButton).toBeEnabled(); + fireEvent.click(createButton); + await wait(() => expect(toasts.addSuccess).toBeCalled()); + expect(mockDynamicActionManager.state.get().events[0].triggers).toEqual(['SELECT_RANGE_TRIGGER']); +}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 283464b137ff9..9fca785ec9072 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -4,16 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { ToastsStart } from 'kibana/public'; import useMountedState from 'react-use/lib/useMountedState'; +import intersection from 'lodash/intersection'; import { DrilldownWizardConfig, FlyoutDrilldownWizard } from '../flyout_drilldown_wizard'; import { FlyoutListManageDrilldowns } from '../flyout_list_manage_drilldowns'; import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public'; -import { - TriggerContextMapping, - APPLY_FILTER_TRIGGER, -} from '../../../../../../../src/plugins/ui_actions/public'; +import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; import { useContainerState } from '../../../../../../../src/plugins/kibana_utils/public'; import { DrilldownListItem } from '../list_manage_drilldowns'; import { @@ -27,15 +25,29 @@ import { } from './i18n'; import { ActionFactory, + BaseActionFactoryContext, DynamicActionManager, SerializedAction, SerializedEvent, } from '../../../dynamic_actions'; +import { ExtraActionFactoryContext } from '../types'; -interface ConnectedFlyoutManageDrilldownsProps { +interface ConnectedFlyoutManageDrilldownsProps< + ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext +> { dynamicActionManager: DynamicActionManager; viewMode?: 'create' | 'manage'; onClose?: () => void; + + /** + * List of possible triggers in current context + */ + supportedTriggers: TriggerId[]; + + /** + * Extra action factory context passed into action factories CollectConfig, getIconType, getDisplayName and etc... + */ + extraContext?: ExtraActionFactoryContext; } /** @@ -52,8 +64,10 @@ export function createFlyoutManageDrilldowns({ storage, toastService, docsLink, + getTrigger, }: { actionFactories: ActionFactory[]; + getTrigger: (triggerId: TriggerId) => Trigger; storage: IStorageWrapper; toastService: ToastsStart; docsLink?: string; @@ -66,19 +80,10 @@ export function createFlyoutManageDrilldowns({ return (props: ConnectedFlyoutManageDrilldownsProps) => { const isCreateOnly = props.viewMode === 'create'; - // TODO: https://github.com/elastic/kibana/issues/59569 - const selectedTriggers: Array = React.useMemo( - () => [APPLY_FILTER_TRIGGER], - [] + const factoryContext: BaseActionFactoryContext = useMemo( + () => ({ ...props.extraContext, triggers: props.supportedTriggers }), + [props.extraContext, props.supportedTriggers] ); - - const factoryContext: object = React.useMemo( - () => ({ - triggers: selectedTriggers, - }), - [selectedTriggers] - ); - const actionFactories = useCompatibleActionFactoriesForCurrentContext( allActionFactories, factoryContext @@ -122,6 +127,7 @@ export function createFlyoutManageDrilldowns({ actionFactory: allActionFactoriesById[drilldownToEdit.action.factoryId], actionConfig: drilldownToEdit.action.config as object, name: drilldownToEdit.action.name, + selectedTriggers: (drilldownToEdit.triggers ?? []) as TriggerId[], }; } @@ -130,16 +136,22 @@ export function createFlyoutManageDrilldowns({ */ function mapToDrilldownToDrilldownListItem(drilldown: SerializedEvent): DrilldownListItem { const actionFactory = allActionFactoriesById[drilldown.action.factoryId]; + const drilldownFactoryContext: BaseActionFactoryContext = { + ...props.extraContext, + triggers: drilldown.triggers as TriggerId[], + }; return { id: drilldown.eventId, drilldownName: drilldown.action.name, - actionName: actionFactory?.getDisplayName(factoryContext) ?? drilldown.action.factoryId, - icon: actionFactory?.getIconType(factoryContext), + actionName: + actionFactory?.getDisplayName(drilldownFactoryContext) ?? drilldown.action.factoryId, + icon: actionFactory?.getIconType(drilldownFactoryContext), error: !actionFactory ? invalidDrilldownType(drilldown.action.factoryId) // this shouldn't happen for the end user, but useful during development : !actionFactory.isCompatibleLicence() ? insufficientLicenseLevel : undefined, + triggers: drilldown.triggers.map((trigger) => getTrigger(trigger as TriggerId)), }; } @@ -155,7 +167,7 @@ export function createFlyoutManageDrilldowns({ onClose={props.onClose} mode={route === Routes.Create ? 'create' : 'edit'} onBack={isCreateOnly ? undefined : () => setRoute(Routes.Manage)} - onSubmit={({ actionConfig, actionFactory, name }) => { + onSubmit={({ actionConfig, actionFactory, name, selectedTriggers }) => { if (route === Routes.Create) { createDrilldown( { @@ -192,13 +204,23 @@ export function createFlyoutManageDrilldowns({ setRoute(Routes.Manage); setCurrentEditId(null); }} - actionFactoryContext={factoryContext} + extraActionFactoryContext={props.extraContext} initialDrilldownWizardConfig={resolveInitialDrilldownWizardConfig()} + supportedTriggers={props.supportedTriggers} + getTrigger={getTrigger} /> ); case Routes.Manage: default: + // show trigger column in case if there is more then 1 possible trigger in current context + const showTriggerColumn = + intersection( + props.supportedTriggers, + actionFactories + .map((factory) => factory.supportedTriggers()) + .reduce((res, next) => res.concat(next), []) + ).length > 1; return ( ); } }; } -function useCompatibleActionFactoriesForCurrentContext( - actionFactories: ActionFactory[], - context: Context -) { +function useCompatibleActionFactoriesForCurrentContext< + Context extends BaseActionFactoryContext = BaseActionFactoryContext +>(actionFactories: ActionFactory[], context: Context) { const [compatibleActionFactories, setCompatibleActionFactories] = useState(); useEffect(() => { let canceled = false; @@ -236,13 +258,18 @@ function useCompatibleActionFactoriesForCurrentContext factory.isCompatible(context)) ); if (canceled) return; - setCompatibleActionFactories(actionFactories.filter((_, i) => compatibility[i])); + + const compatibleFactories = actionFactories.filter((_, i) => compatibility[i]); + const triggerSupportedFactories = compatibleFactories.filter((factory) => + factory.supportedTriggers().some((trigger) => context.triggers.includes(trigger)) + ); + setCompatibleActionFactories(triggerSupportedFactories); } updateCompatibleFactoriesForContext(); return () => { canceled = true; }; - }, [context, actionFactories]); + }, [context, actionFactories, context.triggers]); return compatibleActionFactories; } @@ -280,10 +307,7 @@ function useDrilldownsStateManager(actionManager: DynamicActionManager, toastSer } } - async function createDrilldown( - action: SerializedAction, - selectedTriggers: Array - ) { + async function createDrilldown(action: SerializedAction, selectedTriggers: TriggerId[]) { await run(async () => { await actionManager.createEvent(action, selectedTriggers); toastService.addSuccess({ @@ -296,7 +320,7 @@ function useDrilldownsStateManager(actionManager: DynamicActionManager, toastSer async function editDrilldown( drilldownId: string, action: SerializedAction, - selectedTriggers: Array + selectedTriggers: TriggerId[] ) { await run(async () => { await actionManager.updateEvent(drilldownId, action, selectedTriggers); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx index 01e2a457889ca..8f73c2b3b3cc9 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx @@ -10,12 +10,24 @@ import { storiesOf } from '@storybook/react'; import { FlyoutDrilldownWizard } from './index'; import { dashboardFactory, urlFactory } from '../../../components/action_wizard/test_data'; import { ActionFactory } from '../../../dynamic_actions'; +import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; + +const otherProps = { + supportedTriggers: [ + 'VALUE_CLICK_TRIGGER', + 'SELECT_RANGE_TRIGGER', + 'FILTER_TRIGGER', + ] as TriggerId[], + onClose: () => {}, + getTrigger: (id: TriggerId) => ({ id } as Trigger), +}; storiesOf('components/FlyoutDrilldownWizard', module) .add('default', () => { return ( ); }) @@ -23,11 +35,11 @@ storiesOf('components/FlyoutDrilldownWizard', module) return ( {}}> {}} drilldownActionFactories={[ urlFactory as ActionFactory, dashboardFactory as ActionFactory, ]} + {...otherProps} /> ); @@ -36,7 +48,6 @@ storiesOf('components/FlyoutDrilldownWizard', module) return ( {}}> {}} drilldownActionFactories={[ urlFactory as ActionFactory, dashboardFactory as ActionFactory, @@ -50,6 +61,7 @@ storiesOf('components/FlyoutDrilldownWizard', module) }, }} mode={'edit'} + {...otherProps} /> ); @@ -58,7 +70,6 @@ storiesOf('components/FlyoutDrilldownWizard', module) return ( {}}> {}} drilldownActionFactories={[dashboardFactory as ActionFactory]} initialDrilldownWizardConfig={{ name: 'My fancy drilldown', @@ -69,6 +80,7 @@ storiesOf('components/FlyoutDrilldownWizard', module) }, }} mode={'edit'} + {...otherProps} /> ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index 58cf2501280c7..4b84a177e682c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { EuiButton, EuiSpacer } from '@elastic/eui'; import { FormDrilldownWizard } from '../form_drilldown_wizard'; import { FlyoutFrame } from '../flyout_frame'; @@ -16,15 +16,21 @@ import { txtEditDrilldownTitle, } from './i18n'; import { DrilldownHelloBar } from '../drilldown_hello_bar'; -import { ActionFactory } from '../../../dynamic_actions'; +import { ActionFactory, BaseActionFactoryContext } from '../../../dynamic_actions'; +import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; +import { ExtraActionFactoryContext } from '../types'; export interface DrilldownWizardConfig { name: string; actionFactory?: ActionFactory; actionConfig?: ActionConfig; + selectedTriggers?: TriggerId[]; } -export interface FlyoutDrilldownWizardProps { +export interface FlyoutDrilldownWizardProps< + CurrentActionConfig extends object = object, + ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext +> { drilldownActionFactories: ActionFactory[]; onSubmit?: (drilldownWizardConfig: Required) => void; @@ -38,9 +44,16 @@ export interface FlyoutDrilldownWizardProps void; - actionFactoryContext?: object; + extraActionFactoryContext?: ExtraActionFactoryContext; docsLink?: string; + + getTrigger: (triggerId: TriggerId) => Trigger; + + /** + * List of possible triggers in current context + */ + supportedTriggers: TriggerId[]; } function useWizardConfigState( @@ -51,6 +64,7 @@ function useWizardConfigState( setName: (name: string) => void; setActionConfig: (actionConfig: object) => void; setActionFactory: (actionFactory?: ActionFactory) => void; + setSelectedTriggers: (triggers?: TriggerId[]) => void; } ] { const [wizardConfig, setWizardConfig] = useState( @@ -105,6 +119,12 @@ function useWizardConfigState( }); } }, + setSelectedTriggers: (selectedTriggers: TriggerId[] = []) => { + setWizardConfig({ + ...wizardConfig, + selectedTriggers, + }); + }, }, ]; } @@ -119,12 +139,15 @@ export function FlyoutDrilldownWizard) { - const [wizardConfig, { setActionFactory, setActionConfig, setName }] = useWizardConfigState( - initialDrilldownWizardConfig - ); + const [ + wizardConfig, + { setActionFactory, setActionConfig, setName, setSelectedTriggers }, + ] = useWizardConfigState(initialDrilldownWizardConfig); const isActionValid = ( config: DrilldownWizardConfig @@ -132,10 +155,19 @@ export function FlyoutDrilldownWizard ({ + ...extraActionFactoryContext, + triggers: wizardConfig.selectedTriggers ?? [], + }), + [extraActionFactoryContext, wizardConfig.selectedTriggers] + ); + const footer = ( { @@ -171,7 +203,11 @@ export function FlyoutDrilldownWizard {mode === 'edit' && ( <> diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.tsx index 1d849b1db0688..97face28a5e4c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/flyout_list_manage_drilldowns/flyout_list_manage_drilldowns.tsx @@ -19,6 +19,7 @@ export interface FlyoutListManageDrilldownsProps { onDelete?: (drilldownIds: string[]) => void; showWelcomeMessage?: boolean; onWelcomeHideClick?: () => void; + showTriggerColumn?: boolean; } export function FlyoutListManageDrilldowns({ @@ -30,6 +31,7 @@ export function FlyoutListManageDrilldowns({ onEdit, showWelcomeMessage = true, onWelcomeHideClick, + showTriggerColumn, }: FlyoutListManageDrilldownsProps) { return ( ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.story.tsx index fe63b0835af9e..9ab893f23b398 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.story.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.story.tsx @@ -7,13 +7,25 @@ import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { FormDrilldownWizard } from './index'; +import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; + +const otherProps = { + supportedTriggers: [ + 'VALUE_CLICK_TRIGGER', + 'SELECT_RANGE_TRIGGER', + 'FILTER_TRIGGER', + ] as TriggerId[], + getTriggerInfo: (id: TriggerId) => ({ id } as Trigger), + onSelectedTriggersChange: () => {}, + actionFactoryContext: { triggers: [] as TriggerId[] }, +}; const DemoEditName: React.FC = () => { const [name, setName] = React.useState(''); return ( <> - {' '} + {' '}
name: {name}
); @@ -21,9 +33,9 @@ const DemoEditName: React.FC = () => { storiesOf('components/FormDrilldownWizard', module) .add('default', () => { - return ; + return ; }) .add('[name=foobar]', () => { - return ; + return ; }) .add('can edit name', () => ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx index d9c53ae6f737a..0dcca84ede3bf 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.test.tsx @@ -9,20 +9,32 @@ import { render } from 'react-dom'; import { FormDrilldownWizard } from './form_drilldown_wizard'; import { render as renderTestingLibrary, fireEvent, cleanup } from '@testing-library/react/pure'; import { txtNameOfDrilldown } from './i18n'; +import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; afterEach(cleanup); +const otherProps = { + actionFactoryContext: { triggers: [] as TriggerId[] }, + supportedTriggers: [ + 'VALUE_CLICK_TRIGGER', + 'SELECT_RANGE_TRIGGER', + 'FILTER_TRIGGER', + ] as TriggerId[], + getTriggerInfo: (id: TriggerId) => ({ id } as Trigger), + onSelectedTriggersChange: () => {}, +}; + describe('', () => { test('renders without crashing', () => { const div = document.createElement('div'); - render( {}} actionFactoryContext={{}} />, div); + render( {}} {...otherProps} />, div); }); describe('[name=]', () => { test('if name not provided, uses to empty string', () => { const div = document.createElement('div'); - render(, div); + render(, div); const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement; @@ -32,13 +44,13 @@ describe('', () => { test('can set initial name input field value', () => { const div = document.createElement('div'); - render(, div); + render(, div); const input = div.querySelector('[data-test-subj="drilldownNameInput"]') as HTMLInputElement; expect(input?.value).toBe('foo'); - render(, div); + render(, div); expect(input?.value).toBe('bar'); }); @@ -46,7 +58,7 @@ describe('', () => { test('fires onNameChange callback on name change', () => { const onNameChange = jest.fn(); const utils = renderTestingLibrary( - + ); const input = utils.getByLabelText(txtNameOfDrilldown); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx index e7e7f72dbf58f..bb3eb89d8f199 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/form_drilldown_wizard/form_drilldown_wizard.tsx @@ -8,25 +8,43 @@ import React from 'react'; import { EuiFieldText, EuiForm, EuiFormRow, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { txtDrilldownAction, txtNameOfDrilldown, txtUntitledDrilldown } from './i18n'; -import { ActionFactory } from '../../../dynamic_actions'; +import { ActionFactory, BaseActionFactoryContext } from '../../../dynamic_actions'; import { ActionWizard } from '../../../components/action_wizard'; +import { Trigger, TriggerId } from '../../../../../../../src/plugins/ui_actions/public'; const GET_MORE_ACTIONS_LINK = 'https://www.elastic.co/subscriptions'; const noopFn = () => {}; -export interface FormDrilldownWizardProps { +export interface FormDrilldownWizardProps< + ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext +> { name?: string; onNameChange?: (name: string) => void; currentActionFactory?: ActionFactory; onActionFactoryChange?: (actionFactory?: ActionFactory) => void; - actionFactoryContext: object; + actionFactoryContext: ActionFactoryContext; actionConfig?: object; onActionConfigChange?: (config: object) => void; actionFactories?: ActionFactory[]; + + /** + * Trigger selection has changed + * @param triggers + */ + onSelectedTriggersChange: (triggers?: TriggerId[]) => void; + + getTriggerInfo: (triggerId: TriggerId) => Trigger; + + /** + * List of possible triggers in current context + */ + supportedTriggers: TriggerId[]; + + triggerPickerDocsLink?: string; } export const FormDrilldownWizard: React.FC = ({ @@ -38,6 +56,10 @@ export const FormDrilldownWizard: React.FC = ({ onActionFactoryChange = noopFn, actionFactories = [], actionFactoryContext, + onSelectedTriggersChange, + getTriggerInfo, + supportedTriggers, + triggerPickerDocsLink, }) => { const nameFragment = ( @@ -86,6 +108,10 @@ export const FormDrilldownWizard: React.FC = ({ onActionFactoryChange={(actionFactory) => onActionFactoryChange(actionFactory)} onConfigChange={(config) => onActionConfigChange(config)} context={actionFactoryContext} + onSelectedTriggersChange={onSelectedTriggersChange} + getTriggerInfo={getTriggerInfo} + supportedTriggers={supportedTriggers} + triggerPickerDocsLink={triggerPickerDocsLink} /> ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.story.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.story.tsx index eafe50bab2016..51df6c8d1c715 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.story.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.story.tsx @@ -11,9 +11,26 @@ import { ListManageDrilldowns } from './list_manage_drilldowns'; storiesOf('components/ListManageDrilldowns', module).add('default', () => ( )); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx index b828c4d7d076d..1c5085087791c 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/list_manage_drilldowns/list_manage_drilldowns.tsx @@ -30,6 +30,12 @@ export interface DrilldownListItem { drilldownName: string; icon?: string; error?: string; + triggers?: Trigger[]; +} + +interface Trigger { + title?: string; + description?: string; } export interface ListManageDrilldownsProps { @@ -38,6 +44,8 @@ export interface ListManageDrilldownsProps { onEdit?: (id: string) => void; onCreate?: () => void; onDelete?: (ids: string[]) => void; + + showTriggerColumn?: boolean; } const noop = () => {}; @@ -49,14 +57,13 @@ export function ListManageDrilldowns({ onEdit = noop, onCreate = noop, onDelete = noop, + showTriggerColumn = true, }: ListManageDrilldownsProps) { const [selectedDrilldowns, setSelectedDrilldowns] = useState([]); const columns: Array> = [ { name: 'Name', - truncateText: true, - width: '50%', 'data-test-subj': 'drilldownListItemName', render: (drilldown: DrilldownListItem) => (
@@ -85,21 +92,38 @@ export function ListManageDrilldowns({ )} - + {drilldown.actionName} ), }, + showTriggerColumn && { + name: 'Trigger', + textOnly: true, + render: (drilldown: DrilldownListItem) => + drilldown.triggers?.map((trigger, idx) => + trigger.description ? ( + + {trigger.title ?? 'unknown'} + + ) : ( + + {trigger.title ?? 'unknown'} + + ) + ), + }, { align: 'right', + width: '64px', render: (drilldown: DrilldownListItem) => ( onEdit(drilldown.id)}> {txtEditDrilldown} ), }, - ]; + ].filter(Boolean) as Array>; return ( <> diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts new file mode 100644 index 0000000000000..870b55c24fb58 --- /dev/null +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/types.ts @@ -0,0 +1,15 @@ +/* + * 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 { BaseActionFactoryContext } from '../../dynamic_actions'; + +/** + * Interface used as piece of ActionFactoryContext that is passed in from drilldown wizard component to action factories + * Omitted values are added inside the wizard and then full {@link BaseActionFactoryContext} passed into action factory methods + */ +export type ExtraActionFactoryContext< + ActionFactoryContext extends BaseActionFactoryContext = BaseActionFactoryContext +> = Omit; diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts index 756bdf9e672aa..0efde6214ab2b 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/drilldown_definition.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionFactoryDefinition } from '../dynamic_actions'; +import { ActionFactoryDefinition, BaseActionFactoryContext } from '../dynamic_actions'; import { LicenseType } from '../../../licensing/public'; +import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public'; import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/public'; /** @@ -21,9 +22,14 @@ import { ActionExecutionContext } from '../../../../../src/plugins/ui_actions/pu * and provided to the `execute` function of the drilldown. This object contains * information about the action user performed. */ + export interface DrilldownDefinition< Config extends object = object, - ExecutionContext extends object = object + SupportedTriggers extends TriggerId = TriggerId, + FactoryContext extends BaseActionFactoryContext = { + triggers: SupportedTriggers[]; + }, + ExecutionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] > { /** * Globally unique identifier for this drilldown. @@ -45,7 +51,12 @@ export interface DrilldownDefinition< /** * Function that returns default config for this drilldown. */ - createConfig: ActionFactoryDefinition['createConfig']; + createConfig: ActionFactoryDefinition< + Config, + SupportedTriggers, + FactoryContext, + ExecutionContext + >['createConfig']; /** * `UiComponent` that collections config for this drilldown. You can create @@ -66,13 +77,23 @@ export interface DrilldownDefinition< * export const CollectConfig = uiToReactComponent(ReactCollectConfig); * ``` */ - CollectConfig: ActionFactoryDefinition['CollectConfig']; + CollectConfig: ActionFactoryDefinition< + Config, + SupportedTriggers, + FactoryContext, + ExecutionContext + >['CollectConfig']; /** * A validator function for the config object. Should always return a boolean * given any input. */ - isConfigValid: ActionFactoryDefinition['isConfigValid']; + isConfigValid: ActionFactoryDefinition< + Config, + SupportedTriggers, + FactoryContext, + ExecutionContext + >['isConfigValid']; /** * Name of EUI icon to display when showing this drilldown to user. @@ -106,4 +127,10 @@ export interface DrilldownDefinition< config: Config, context: ExecutionContext | ActionExecutionContext ): Promise; + + /** + * List of triggers supported by this drilldown type + * This is used in trigger picker when configuring drilldown + */ + supportedTriggers(): SupportedTriggers[]; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts index 918c6422546f4..159e8be95f272 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.test.ts @@ -19,12 +19,13 @@ const def: ActionFactoryDefinition = { getDisplayName: () => name, enhancements: {}, }), + supportedTriggers: () => [], }; describe('License & ActionFactory', () => { test('no license requirements', async () => { const factory = new ActionFactory(def, () => licensingMock.createLicense()); - expect(await factory.isCompatible({})).toBe(true); + expect(await factory.isCompatible({ triggers: [] })).toBe(true); expect(factory.isCompatibleLicence()).toBe(true); }); @@ -32,7 +33,7 @@ describe('License & ActionFactory', () => { const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => licensingMock.createLicense() ); - expect(await factory.isCompatible({})).toBe(true); + expect(await factory.isCompatible({ triggers: [] })).toBe(true); expect(factory.isCompatibleLicence()).toBe(false); }); @@ -40,7 +41,7 @@ describe('License & ActionFactory', () => { const factory = new ActionFactory({ ...def, minimalLicense: 'gold' }, () => licensingMock.createLicense({ license: { type: 'gold' } }) ); - expect(await factory.isCompatible({})).toBe(true); + expect(await factory.isCompatible({ triggers: [] })).toBe(true); expect(factory.isCompatibleLicence()).toBe(true); }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts index 95b7941b48ed3..cb764d99b4a03 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory.ts @@ -5,20 +5,32 @@ */ import { uiToReactComponent } from '../../../../../src/plugins/kibana_react/public'; -import { UiActionsPresentable as Presentable } from '../../../../../src/plugins/ui_actions/public'; +import { + TriggerContextMapping, + TriggerId, + UiActionsPresentable as Presentable, +} from '../../../../../src/plugins/ui_actions/public'; import { ActionFactoryDefinition } from './action_factory_definition'; import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; -import { SerializedAction } from './types'; +import { BaseActionFactoryContext, SerializedAction } from './types'; import { ILicense } from '../../../licensing/public'; import { UiActionsActionDefinition as ActionDefinition } from '../../../../../src/plugins/ui_actions/public'; export class ActionFactory< Config extends object = object, - FactoryContext extends object = object, - ActionContext extends object = object + SupportedTriggers extends TriggerId = TriggerId, + FactoryContext extends BaseActionFactoryContext = { + triggers: SupportedTriggers[]; + }, + ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] > implements Omit, 'getHref'>, Configurable { constructor( - protected readonly def: ActionFactoryDefinition, + protected readonly def: ActionFactoryDefinition< + Config, + SupportedTriggers, + FactoryContext, + ActionContext + >, protected readonly getLicence: () => ILicense ) {} @@ -74,4 +86,8 @@ export class ActionFactory< }, }; } + + public supportedTriggers(): SupportedTriggers[] { + return this.def.supportedTriggers(); + } } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts index d63f69ba5ab72..0acd3ea3e51a7 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/action_factory_definition.ts @@ -5,9 +5,11 @@ */ import { Configurable } from '../../../../../src/plugins/kibana_utils/public'; -import { SerializedAction } from './types'; +import { BaseActionFactoryContext, SerializedAction } from './types'; import { LicenseType } from '../../../licensing/public'; import { + TriggerContextMapping, + TriggerId, UiActionsActionDefinition as ActionDefinition, UiActionsPresentable as Presentable, } from '../../../../../src/plugins/ui_actions/public'; @@ -17,8 +19,11 @@ import { */ export interface ActionFactoryDefinition< Config extends object = object, - FactoryContext extends object = object, - ActionContext extends object = object + SupportedTriggers extends TriggerId = TriggerId, + FactoryContext extends BaseActionFactoryContext = { + triggers: SupportedTriggers[]; + }, + ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] > extends Partial, 'getHref'>>, Configurable { @@ -42,4 +47,6 @@ export interface ActionFactoryDefinition< create( serializedAction: Omit, 'factoryId'> ): ActionDefinition; + + supportedTriggers(): SupportedTriggers[]; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index 930f88ff08775..0b0cd39e35e25 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -24,6 +24,9 @@ const actionFactoryDefinition1: ActionFactoryDefinition = { execute: async () => {}, getDisplayName: () => name, }), + supportedTriggers() { + return ['VALUE_CLICK_TRIGGER']; + }, }; const actionFactoryDefinition2: ActionFactoryDefinition = { @@ -36,6 +39,9 @@ const actionFactoryDefinition2: ActionFactoryDefinition = { execute: async () => {}, getDisplayName: () => name, }), + supportedTriggers() { + return ['VALUE_CLICK_TRIGGER']; + }, }; const event1: SerializedEvent = { @@ -417,6 +423,21 @@ describe('DynamicActionManager', () => { expect(actions.size).toBe(0); }); + + test('throws when trigger is unknown', async () => { + const { manager, uiActions } = setup([]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + await manager.start(); + + const action: SerializedAction = { + factoryId: actionFactoryDefinition1.id, + name: 'foo', + config: {}, + }; + + await expect(manager.createEvent(action, ['SELECT_RANGE_TRIGGER'])).rejects; + }); }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index 4afefe3006a43..6ca388281ad76 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -84,7 +84,17 @@ export class DynamicActionManager { return actionDefinition.isCompatible(context); }, }); - for (const trigger of triggers) uiActions.attachAction(trigger as any, actionId); + + const supportedTriggers = factory.supportedTriggers(); + for (const trigger of triggers) { + if (!supportedTriggers.includes(trigger as any)) + throw new Error( + `Can't attach [action=${actionId}] to [trigger=${trigger}]. Supported triggers for this action: ${supportedTriggers.join( + ',' + )}` + ); + uiActions.attachAction(trigger as any, actionId); + } } protected killAction({ eventId, triggers }: SerializedEvent) { diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts index fb513e892d413..d00db0d9acb7a 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TriggerId } from '../../../../../src/plugins/ui_actions/public'; + export interface SerializedAction { readonly factoryId: string; readonly name: string; @@ -18,3 +20,10 @@ export interface SerializedEvent { triggers: string[]; action: SerializedAction; } + +/** + * Action factory context passed into ActionFactories' CollectConfig, getDisplayName, getIconType + */ +export interface BaseActionFactoryContext { + triggers: SupportedTriggers[]; +} diff --git a/x-pack/plugins/ui_actions_enhanced/public/index.ts b/x-pack/plugins/ui_actions_enhanced/public/index.ts index a3cfddb31d663..a255bc28f5c68 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/index.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/index.ts @@ -28,6 +28,7 @@ export { DynamicActionManagerParams as UiActionsEnhancedDynamicActionManagerParams, DynamicActionManagerState as UiActionsEnhancedDynamicActionManagerState, MemoryActionStorage as UiActionsEnhancedMemoryActionStorage, + BaseActionFactoryContext as UiActionsEnhancedBaseActionFactoryContext, } from './dynamic_actions'; export { DrilldownDefinition as UiActionsEnhancedDrilldownDefinition } from './drilldowns'; diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index a625ea2e2118b..5069b485b198d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -13,7 +13,11 @@ import { } from '../../../../src/core/public'; import { createReactOverlays } from '../../../../src/plugins/kibana_react/public'; import { UI_SETTINGS } from '../../../../src/plugins/data/public'; -import { UiActionsSetup, UiActionsStart } from '../../../../src/plugins/ui_actions/public'; +import { + TriggerId, + UiActionsSetup, + UiActionsStart, +} from '../../../../src/plugins/ui_actions/public'; import { CONTEXT_MENU_TRIGGER, PANEL_BADGE_TRIGGER, @@ -116,6 +120,7 @@ export class AdvancedUiActionsPublicPlugin ...this.enhancements, FlyoutManageDrilldowns: createFlyoutManageDrilldowns({ actionFactories: this.enhancements.getActionFactories(), + getTrigger: (triggerId: TriggerId) => uiActions.getTrigger(triggerId), storage: new Storage(window?.localStorage), toastService: core.notifications.toasts, docsLink: core.docLinks.links.dashboard.drilldowns, diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts index 4f2ddcf7e0491..ab17c3f549dcd 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.test.ts @@ -18,6 +18,9 @@ describe('UiActionsService', () => { createConfig: () => ({}), isConfigValid: () => true, create: () => ({} as any), + supportedTriggers() { + return ['VALUE_CLICK_TRIGGER']; + }, }; const factoryDefinition2: ActionFactoryDefinition = { id: 'test-factory-2', @@ -25,6 +28,9 @@ describe('UiActionsService', () => { createConfig: () => ({}), isConfigValid: () => true, create: () => ({} as any), + supportedTriggers() { + return ['VALUE_CLICK_TRIGGER']; + }, }; test('.getActionFactories() returns empty array if no action factories registered', () => { diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index bd05659d59e9d..10786697243dc 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -5,9 +5,14 @@ */ import { ActionFactoryRegistry } from '../types'; -import { ActionFactory, ActionFactoryDefinition } from '../dynamic_actions'; +import { + ActionFactory, + ActionFactoryDefinition, + BaseActionFactoryContext, +} from '../dynamic_actions'; import { DrilldownDefinition } from '../drilldowns'; import { ILicense } from '../../../licensing/common/types'; +import { TriggerContextMapping, TriggerId } from '../../../../../src/plugins/ui_actions/public'; export interface UiActionsServiceEnhancementsParams { readonly actionFactories?: ActionFactoryRegistry; @@ -29,19 +34,24 @@ export class UiActionsServiceEnhancements { */ public readonly registerActionFactory = < Config extends object = object, - FactoryContext extends object = object, - ActionContext extends object = object + SupportedTriggers extends TriggerId = TriggerId, + FactoryContext extends BaseActionFactoryContext = { + triggers: SupportedTriggers[]; + }, + ActionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] >( - definition: ActionFactoryDefinition + definition: ActionFactoryDefinition ) => { if (this.actionFactories.has(definition.id)) { throw new Error(`ActionFactory [actionFactory.id = ${definition.id}] already registered.`); } - const actionFactory = new ActionFactory( - definition, - this.getLicenseInfo - ); + const actionFactory = new ActionFactory< + Config, + SupportedTriggers, + FactoryContext, + ActionContext + >(definition, this.getLicenseInfo); this.actionFactories.set(actionFactory.id, actionFactory as ActionFactory); }; @@ -68,7 +78,11 @@ export class UiActionsServiceEnhancements { */ public readonly registerDrilldown = < Config extends object = object, - ExecutionContext extends object = object + SupportedTriggers extends TriggerId = TriggerId, + FactoryContext extends BaseActionFactoryContext = { + triggers: SupportedTriggers[]; + }, + ExecutionContext extends TriggerContextMapping[SupportedTriggers] = TriggerContextMapping[SupportedTriggers] >({ id: factoryId, order, @@ -80,8 +94,14 @@ export class UiActionsServiceEnhancements { execute, getHref, minimalLicense, - }: DrilldownDefinition): void => { - const actionFactory: ActionFactoryDefinition = { + supportedTriggers, + }: DrilldownDefinition): void => { + const actionFactory: ActionFactoryDefinition< + Config, + SupportedTriggers, + FactoryContext, + ExecutionContext + > = { id: factoryId, minimalLicense, order, @@ -89,6 +109,7 @@ export class UiActionsServiceEnhancements { createConfig, isConfigValid, getDisplayName, + supportedTriggers, getIconType: () => euiIcon, isCompatible: async () => true, create: (serializedAction) => ({ @@ -99,7 +120,7 @@ export class UiActionsServiceEnhancements { execute: async (context) => await execute(serializedAction.config, context), getHref: getHref ? async (context) => getHref(serializedAction.config, context) : undefined, }), - } as ActionFactoryDefinition; + } as ActionFactoryDefinition; this.registerActionFactory(actionFactory); }; diff --git a/x-pack/test/alerting_api_integration/basic/config.ts b/x-pack/test/alerting_api_integration/basic/config.ts index f9c248ec3d56f..f58b7753b74f7 100644 --- a/x-pack/test/alerting_api_integration/basic/config.ts +++ b/x-pack/test/alerting_api_integration/basic/config.ts @@ -11,4 +11,5 @@ export default createTestConfig('basic', { disabledPlugins: [], license: 'basic', ssl: true, + enableActionsProxy: false, }); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 4947cdbf55484..34e23a2dba0b2 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -5,6 +5,7 @@ */ import path from 'path'; +import getPort from 'get-port'; import fs from 'fs'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; @@ -15,6 +16,7 @@ interface CreateTestConfigOptions { license: string; disabledPlugins?: string[]; ssl?: boolean; + enableActionsProxy: boolean; } // test.not-enabled is specifically not enabled @@ -56,6 +58,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory() ); + const actionsProxyUrl = options.enableActionsProxy + ? [`--xpack.actions.proxyUrl=http://localhost:${await getPort()}`] + : []; + return { testFiles: [require.resolve(`../${name}/tests/`)], servers, @@ -85,6 +91,9 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ])}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + ...actionsProxyUrl, + '--xpack.actions.rejectUnauthorizedCertificates=false', + '--xpack.eventLog.logEntries=true', `--xpack.actions.preconfigured=${JSON.stringify({ 'my-slack1': { diff --git a/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts new file mode 100644 index 0000000000000..4540556e73c5f --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/lib/get_proxy_server.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import httpProxy from 'http-proxy'; + +export const getHttpProxyServer = ( + targetUrl: string, + onProxyResHandler: (proxyRes?: unknown, req?: unknown, res?: unknown) => void +): httpProxy => { + const proxyServer = httpProxy.createProxyServer({ + target: targetUrl, + secure: false, + selfHandleResponse: false, + }); + proxyServer.on('proxyRes', (proxyRes: unknown, req: unknown, res: unknown) => { + onProxyResHandler(proxyRes, req, res); + }); + return proxyServer; +}; + +export const getProxyUrl = (kbnTestServerConfig: any) => { + const proxyUrl = kbnTestServerConfig + .find((val: string) => val.startsWith('--xpack.actions.proxyUrl=')) + .replace('--xpack.actions.proxyUrl=', ''); + + return new URL(proxyUrl); +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts b/x-pack/test/alerting_api_integration/security_and_spaces/config.ts index 081b901c47fc3..97f53ae2c3664 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/config.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/config.ts @@ -11,4 +11,5 @@ export default createTestConfig('security_and_spaces', { disabledPlugins: [], license: 'trial', ssl: true, + enableActionsProxy: true, }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 24931f11d4999..a0ba5331105bc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +36,7 @@ const mapping = [ export default function jiraTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); const mockJira = { config: { @@ -73,12 +75,19 @@ export default function jiraTest({ getService }: FtrProviderContext) { }; let jiraSimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; describe('Jira', () => { before(() => { jiraSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) ); + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); describe('Jira - Action Creation', () => { @@ -529,6 +538,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -542,5 +553,9 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts index f4fcbb65ab5a3..c697cf69bb4d5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/pagerduty.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -17,16 +18,25 @@ import { export default function pagerdutyTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); describe('pagerduty action', () => { let simulatedActionId = ''; let pagerdutySimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... before(() => { pagerdutySimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY) ); + + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); it('should return successfully when passed valid create parameters', async () => { @@ -144,6 +154,8 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { }, }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); + expect(result).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -202,5 +214,9 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) { expect(result.message).to.match(/error posting pagerduty event: http status 502/); expect(result.retry).to.equal(true); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 94feabb556a51..5085c87550d01 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +36,7 @@ const mapping = [ export default function resilientTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); const mockResilient = { config: { @@ -73,12 +75,19 @@ export default function resilientTest({ getService }: FtrProviderContext) { }; let resilientSimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; describe('IBM Resilient', () => { before(() => { resilientSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT) ); + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); describe('IBM Resilient - Action Creation', () => { @@ -529,6 +538,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ status: 'ok', actionId: simulatedActionId, @@ -542,5 +553,9 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index d3b72d01216d0..70b6a8fe512e1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -35,6 +36,7 @@ const mapping = [ export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); + const config = getService('config'); const mockServiceNow = { config: { @@ -72,12 +74,20 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }; let servicenowSimulatorURL: string = ''; + let proxyServer: any; + let proxyHaveBeenCalled = false; describe('ServiceNow', () => { before(() => { servicenowSimulatorURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) ); + + proxyServer = getHttpProxyServer(kibanaServer.resolveUrl('/'), () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); describe('ServiceNow - Action Creation', () => { @@ -448,6 +458,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, }) .expect(200); + expect(proxyHaveBeenCalled).to.equal(true); expect(result).to.eql({ status: 'ok', @@ -462,5 +473,9 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); + + after(() => { + proxyServer.close(); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts index c68bcaa0ad4e8..45f9ba369dc23 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/slack.ts @@ -7,6 +7,7 @@ import expect from '@kbn/expect'; import http from 'http'; import getPort from 'get-port'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; @@ -14,18 +15,27 @@ import { getSlackServer } from '../../../../common/fixtures/plugins/actions_simu // eslint-disable-next-line import/no-default-export export default function slackTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); + const config = getService('config'); describe('slack action', () => { let simulatedActionId = ''; let slackSimulatorURL: string = ''; let slackServer: http.Server; + let proxyServer: any; + let proxyHaveBeenCalled = false; // need to wait for kibanaServer to settle ... before(async () => { slackServer = await getSlackServer(); const availablePort = await getPort({ port: 9000 }); slackServer.listen(availablePort); slackSimulatorURL = `http://localhost:${availablePort}`; + + proxyServer = getHttpProxyServer(slackSimulatorURL, () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(config.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); }); it('should return 200 when creating a slack action successfully', async () => { @@ -155,6 +165,7 @@ export default function slackTest({ getService }: FtrProviderContext) { }) .expect(200); expect(result.status).to.eql('ok'); + expect(proxyHaveBeenCalled).to.equal(true); }); it('should handle an empty message error', async () => { @@ -222,6 +233,7 @@ export default function slackTest({ getService }: FtrProviderContext) { after(() => { slackServer.close(); + proxyServer.close(); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts index 8f17ab54184b5..896026611043f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/webhook.ts @@ -8,6 +8,7 @@ import http from 'http'; import getPort from 'get-port'; import expect from '@kbn/expect'; import { URL, format as formatUrl } from 'url'; +import { getHttpProxyServer, getProxyUrl } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { getExternalServiceSimulatorPath, @@ -31,6 +32,7 @@ function parsePort(url: Record): Record { webhookServer = await getWebhookServer(); @@ -76,6 +80,12 @@ export default function webhookTest({ getService }: FtrProviderContext) { webhookServer.listen(availablePort); webhookSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = getHttpProxyServer(webhookSimulatorURL, () => { + proxyHaveBeenCalled = true; + }); + const proxyUrl = getProxyUrl(configService.get('kbnTestServer.serverArgs')); + proxyServer.listen(Number(proxyUrl.port)); + kibanaURL = kibanaServer.resolveUrl( getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK) ); @@ -140,6 +150,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { .expect(200); expect(result.status).to.eql('ok'); + expect(proxyHaveBeenCalled).to.equal(true); }); it('should support the POST method against webhook target', async () => { @@ -218,7 +229,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { .expect(200); expect(result.status).to.eql('error'); - expect(result.message).to.match(/error calling webhook, unexpected error/); + expect(result.message).to.match(/error calling webhook, retry later/); }); it('should handle failing webhook targets', async () => { @@ -240,6 +251,7 @@ export default function webhookTest({ getService }: FtrProviderContext) { after(() => { webhookServer.close(); + proxyServer.close(); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/config.ts b/x-pack/test/alerting_api_integration/spaces_only/config.ts index c79c26ef68752..f9860b642f13a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/config.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/config.ts @@ -7,4 +7,8 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export -export default createTestConfig('spaces_only', { disabledPlugins: ['security'], license: 'trial' }); +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, +}); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index a62bfdcde0572..beb43c557a0e8 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -12,7 +12,8 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - describe('classification creation', function () { + // failing test due to backend issue, see #75095 + describe.skip('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts index e8f0a69b397cd..4284ad20b951f 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/cloning.ts @@ -13,7 +13,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const ml = getService('ml'); - describe('jobs cloning supported by UI form', function () { + // failing test due to backend issue, see #75095 + describe.skip('jobs cloning supported by UI form', function () { const testDataList: Array<{ suiteTitle: string; archive: string; diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index a67a348323347..3cbfad6b33c5f 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -12,7 +12,8 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - describe('regression creation', function () { + // failing test due to backend issue, see #75095 + describe.skip('regression creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index ca5fb888e67e1..6662bfb17949c 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -28,7 +28,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); - describe('Listing of Reports', function () { + // FLAKY: https://github.com/elastic/kibana/issues/75044 + describe.skip('Listing of Reports', function () { before(async () => { await security.testUser.setRoles(['kibana_admin', 'reporting_user']); await esArchiver.load('empty_kibana'); diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts index 7fb8b0a2b1708..e575d7b680301 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/install_remove_assets.ts @@ -61,6 +61,16 @@ export default function (providerContext: FtrProviderContext) { path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}`, }); expect(res.statusCode).equal(200); + const resPipeline1 = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline1`, + }); + expect(resPipeline1.statusCode).equal(200); + const resPipeline2 = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline2`, + }); + expect(resPipeline2.statusCode).equal(200); }); it('should have installed the template components', async function () { const res = await es.transport.request({ @@ -106,18 +116,6 @@ export default function (providerContext: FtrProviderContext) { }); expect(resSearch.id).equal('sample_search'); }); - it('should have installed placeholder indices', async function () { - const resLogsIndexPatternPlaceholder = await es.transport.request({ - method: 'GET', - path: `/logs-index_pattern_placeholder`, - }); - expect(resLogsIndexPatternPlaceholder.statusCode).equal(200); - const resMetricsIndexPatternPlaceholder = await es.transport.request({ - method: 'GET', - path: `/metrics-index_pattern_placeholder`, - }); - expect(resMetricsIndexPatternPlaceholder.statusCode).equal(200); - }); it('should have created the correct saved object', async function () { const res = await kibanaServer.savedObjects.get({ type: 'epm-packages', @@ -147,6 +145,14 @@ export default function (providerContext: FtrProviderContext) { id: 'logs-all_assets.test_logs-0.1.0', type: 'ingest_pipeline', }, + { + id: 'logs-all_assets.test_logs-0.1.0-pipeline1', + type: 'ingest_pipeline', + }, + { + id: 'logs-all_assets.test_logs-0.1.0-pipeline2', + type: 'ingest_pipeline', + }, { id: 'logs-all_assets.test_logs', type: 'index_template', @@ -207,6 +213,26 @@ export default function (providerContext: FtrProviderContext) { } ); expect(res.statusCode).equal(404); + const resPipeline1 = await es.transport.request( + { + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline1`, + }, + { + ignore: [404], + } + ); + expect(resPipeline1.statusCode).equal(404); + const resPipeline2 = await es.transport.request( + { + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline2`, + }, + { + ignore: [404], + } + ); + expect(resPipeline2.statusCode).equal(404); }); it('should have uninstalled the kibana assets', async function () { let resDashboard; diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts index 59ad7a9744ae1..8ad6fe12dcd43 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/update_assets.ts @@ -154,24 +154,49 @@ export default function (providerContext: FtrProviderContext) { }, }); }); - it('should have installed the new versionized pipeline', async function () { + it('should have installed the new versionized pipelines', async function () { const res = await es.transport.request({ method: 'GET', path: `/_ingest/pipeline/${logsTemplateName}-${pkgUpdateVersion}`, }); expect(res.statusCode).equal(200); + const resPipeline1 = await es.transport.request({ + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgUpdateVersion}-pipeline1`, + }); + expect(resPipeline1.statusCode).equal(200); }); it('should have removed the old versionized pipelines', async function () { - let res; - try { - res = await es.transport.request({ + const res = await es.transport.request( + { method: 'GET', path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}`, - }); - } catch (err) { - res = err; - } + }, + { + ignore: [404], + } + ); expect(res.statusCode).equal(404); + const resPipeline1 = await es.transport.request( + { + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline1`, + }, + { + ignore: [404], + } + ); + expect(resPipeline1.statusCode).equal(404); + const resPipeline2 = await es.transport.request( + { + method: 'GET', + path: `/_ingest/pipeline/${logsTemplateName}-${pkgVersion}-pipeline2`, + }, + { + ignore: [404], + } + ); + expect(resPipeline2.statusCode).equal(404); }); it('should have updated the template components', async function () { const res = await es.transport.request({ @@ -272,6 +297,10 @@ export default function (providerContext: FtrProviderContext) { id: 'logs-all_assets.test_logs-0.2.0', type: 'ingest_pipeline', }, + { + id: 'logs-all_assets.test_logs-0.2.0-pipeline1', + type: 'ingest_pipeline', + }, { id: 'logs-all_assets.test_logs', type: 'index_template', diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml new file mode 100644 index 0000000000000..c2471c56ee22a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml @@ -0,0 +1,9 @@ +--- +description: Pipeline test +processors: +- remove: + field: messag +on_failure: + - set: + field: error.message + value: "{{ _ingest.on_failure_message }}" \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline2.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline2.yml new file mode 100644 index 0000000000000..c2471c56ee22a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline2.yml @@ -0,0 +1,9 @@ +--- +description: Pipeline test +processors: +- remove: + field: messag +on_failure: + - set: + field: error.message + value: "{{ _ingest.on_failure_message }}" \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml new file mode 100644 index 0000000000000..c2471c56ee22a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/dataset/test_logs/elasticsearch/ingest_pipeline/pipeline1.yml @@ -0,0 +1,9 @@ +--- +description: Pipeline test +processors: +- remove: + field: messag +on_failure: + - set: + field: error.message + value: "{{ _ingest.on_failure_message }}" \ No newline at end of file diff --git a/x-pack/test/ingest_manager_api_integration/apis/index.js b/x-pack/test/ingest_manager_api_integration/apis/index.js index 72121b2164bfd..948f953ebe3f5 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/index.js @@ -7,7 +7,8 @@ export default function ({ loadTestFile }) { describe('Ingest Manager Endpoints', function () { this.tags('ciGroup7'); - + // Ingest Manager setup + loadTestFile(require.resolve('./setup')); // Fleet loadTestFile(require.resolve('./fleet/index')); diff --git a/x-pack/test/ingest_manager_api_integration/apis/setup.ts b/x-pack/test/ingest_manager_api_integration/apis/setup.ts new file mode 100644 index 0000000000000..daf0c8937715f --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/setup.ts @@ -0,0 +1,32 @@ +/* + * 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 '../../api_integration/ftr_provider_context'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const es = getService('es'); + describe('ingest manager setup', async () => { + before(async () => { + await supertest.post(`/api/ingest_manager/setup`).set('kbn-xsrf', 'xxx').send(); + }); + + it('should have installed placeholder indices', async function () { + const resLogsIndexPatternPlaceholder = await es.transport.request({ + method: 'GET', + path: `/logs-index_pattern_placeholder`, + }); + expect(resLogsIndexPatternPlaceholder.statusCode).equal(200); + const resMetricsIndexPatternPlaceholder = await es.transport.request({ + method: 'GET', + path: `/metrics-index_pattern_placeholder`, + }); + expect(resMetricsIndexPatternPlaceholder.statusCode).equal(200); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index 41323c5bb2761..81bb7338e615f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3913,6 +3913,20 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== +"@types/http-proxy-agent@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/http-proxy-agent/-/http-proxy-agent-2.0.2.tgz#942c1f35c7e1f0edd1b6ffae5d0f9051cfb32be1" + integrity sha512-2S6IuBRhqUnH1/AUx9k8KWtY3Esg4eqri946MnxTG5HwehF1S5mqLln8fcyMiuQkY72p2gH3W+rIPqp5li0LyQ== + dependencies: + "@types/node" "*" + +"@types/http-proxy@^1.17.4": + version "1.17.4" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b" + integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q== + dependencies: + "@types/node" "*" + "@types/inert@^5.1.2": version "5.1.2" resolved "https://registry.yarnpkg.com/@types/inert/-/inert-5.1.2.tgz#2bb8bef3b2462f904c960654c9edfa39285a85c6"