From fa16b2b8495ad3c0e63a2d5369130c8929e5777f Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Wed, 11 Mar 2020 08:34:22 +0000 Subject: [PATCH 01/13] Alerting/fix flaky instance test (#58994) The Alert Instances list calculates duration on each page load, which makes it hard for the test runner to know what the correct value should be. In the PR we expose the epoch used by the duration calculation in such a way that the test runner can read it and asses the duration correctly. --- .../components/alert_details.test.tsx | 2 +- .../components/alert_details.tsx | 4 +- .../components/alert_instances.test.tsx | 50 +++++++++++---- .../components/alert_instances.tsx | 61 ++++++++++++------- .../apps/triggers_actions_ui/details.ts | 27 +++++--- .../page_objects/alert_details.ts | 6 ++ 6 files changed, 104 insertions(+), 46 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx index 2625768dc7242..64069009f6589 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.test.tsx @@ -63,7 +63,7 @@ describe('alert_details', () => { ).containsMatchingElement(

- {alert.name} + {alert.name} = ({ -

- {alert.name} +

+ {alert.name} { const alertState = mockAlertState(); const instances: AlertInstanceListItem[] = [ - alertInstanceToListItem(alert, 'first_instance', alertState.alertInstances!.first_instance), - alertInstanceToListItem(alert, 'second_instance', alertState.alertInstances!.second_instance), + alertInstanceToListItem( + fakeNow.getTime(), + alert, + 'first_instance', + alertState.alertInstances!.first_instance + ), + alertInstanceToListItem( + fakeNow.getTime(), + alert, + 'second_instance', + alertState.alertInstances!.second_instance + ), ]; expect( @@ -48,6 +58,24 @@ describe('alert_instances', () => { ).toEqual(instances); }); + it('render a hidden field with duration epoch', () => { + const alert = mockAlert(); + const alertState = mockAlertState(); + + expect( + shallow( + + ) + .find('[name="alertInstancesDurationEpoch"]') + .prop('value') + ).toEqual(fake2MinutesAgo.getTime()); + }); + it('render all active alert instances', () => { const alert = mockAlert(); const instances = { @@ -75,8 +103,8 @@ describe('alert_instances', () => { .find(EuiBasicTable) .prop('items') ).toEqual([ - alertInstanceToListItem(alert, 'us-central', instances['us-central']), - alertInstanceToListItem(alert, 'us-east', instances['us-east']), + alertInstanceToListItem(fakeNow.getTime(), alert, 'us-central', instances['us-central']), + alertInstanceToListItem(fakeNow.getTime(), alert, 'us-east', instances['us-east']), ]); }); @@ -98,8 +126,8 @@ describe('alert_instances', () => { .find(EuiBasicTable) .prop('items') ).toEqual([ - alertInstanceToListItem(alert, 'us-west'), - alertInstanceToListItem(alert, 'us-east'), + alertInstanceToListItem(fakeNow.getTime(), alert, 'us-west'), + alertInstanceToListItem(fakeNow.getTime(), alert, 'us-east'), ]); }); }); @@ -117,7 +145,7 @@ describe('alertInstanceToListItem', () => { }, }; - expect(alertInstanceToListItem(alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ instance: 'id', status: { label: 'Active', healthColor: 'primary' }, start, @@ -140,7 +168,7 @@ describe('alertInstanceToListItem', () => { }, }; - expect(alertInstanceToListItem(alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ instance: 'id', status: { label: 'Active', healthColor: 'primary' }, start, @@ -153,7 +181,7 @@ describe('alertInstanceToListItem', () => { const alert = mockAlert(); const instance: RawAlertInstance = {}; - expect(alertInstanceToListItem(alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ instance: 'id', status: { label: 'Active', healthColor: 'primary' }, start: undefined, @@ -168,7 +196,7 @@ describe('alertInstanceToListItem', () => { meta: {}, }; - expect(alertInstanceToListItem(alert, 'id', instance)).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id', instance)).toEqual({ instance: 'id', status: { label: 'Active', healthColor: 'primary' }, start: undefined, @@ -181,7 +209,7 @@ describe('alertInstanceToListItem', () => { const alert = mockAlert({ mutedInstanceIds: ['id'], }); - expect(alertInstanceToListItem(alert, 'id')).toEqual({ + expect(alertInstanceToListItem(fakeNow.getTime(), alert, 'id')).toEqual({ instance: 'id', status: { label: 'Inactive', healthColor: 'subdued' }, start: undefined, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index 98aa981f40d11..fa4d8f66cd7bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -23,6 +23,7 @@ type AlertInstancesProps = { alert: Alert; alertState: AlertTaskState; requestRefresh: () => Promise; + durationEpoch?: number; } & Pick; export const alertInstancesTableColumns = ( @@ -134,6 +135,7 @@ export function AlertInstances({ muteAlertInstance, unmuteAlertInstance, requestRefresh, + durationEpoch = Date.now(), }: AlertInstancesProps) { const [pagination, setPagination] = useState({ index: 0, @@ -142,10 +144,10 @@ export function AlertInstances({ const mergedAlertInstances = [ ...Object.entries(alertInstances).map(([instanceId, instance]) => - alertInstanceToListItem(alert, instanceId, instance) + alertInstanceToListItem(durationEpoch, alert, instanceId, instance) ), ...difference(alert.mutedInstanceIds, Object.keys(alertInstances)).map(instanceId => - alertInstanceToListItem(alert, instanceId) + alertInstanceToListItem(durationEpoch, alert, instanceId) ), ]; const pageOfAlertInstances = getPage(mergedAlertInstances, pagination); @@ -158,25 +160,33 @@ export function AlertInstances({ }; return ( - { - setPagination(changedPage); - }} - rowProps={() => ({ - 'data-test-subj': 'alert-instance-row', - })} - cellProps={() => ({ - 'data-test-subj': 'cell', - })} - columns={alertInstancesTableColumns(onMuteAction)} - data-test-subj="alertInstancesList" - /> + + + { + setPagination(changedPage); + }} + rowProps={() => ({ + 'data-test-subj': 'alert-instance-row', + })} + cellProps={() => ({ + 'data-test-subj': 'cell', + })} + columns={alertInstancesTableColumns(onMuteAction)} + data-test-subj="alertInstancesList" + /> + ); } export const AlertInstancesWithApi = withBulkAlertOperations(AlertInstances); @@ -207,9 +217,11 @@ const INACTIVE_LABEL = i18n.translate( { defaultMessage: 'Inactive' } ); -const durationSince = (start?: Date) => (start ? Date.now() - start.getTime() : 0); +const durationSince = (durationEpoch: number, startTime?: number) => + startTime ? durationEpoch - startTime : 0; export function alertInstanceToListItem( + durationEpoch: number, alert: Alert, instanceId: string, instance?: RawAlertInstance @@ -221,7 +233,10 @@ export function alertInstanceToListItem( ? { label: ACTIVE_LABEL, healthColor: 'primary' } : { label: INACTIVE_LABEL, healthColor: 'subdued' }, start: instance?.meta?.lastScheduledActions?.date, - duration: durationSince(instance?.meta?.lastScheduledActions?.date), + duration: durationSince( + durationEpoch, + instance?.meta?.lastScheduledActions?.date?.getTime() ?? 0 + ), isMuted, }; } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts index 86fc3d6cd6a6c..74a267c6e0a8e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/details.ts @@ -18,8 +18,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const alerting = getService('alerting'); const retry = getService('retry'); - // FLAKY: https://github.com/elastic/kibana/issues/57426 - describe.skip('Alert Details', function() { + describe('Alert Details', function() { describe('Header', function() { const testRunUuid = uuid.v4(); before(async () => { @@ -206,8 +205,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('renders the active alert instances', async () => { - const testBeganAt = moment().utc(); - // Verify content await testSubjects.existOrFail('alertInstancesList'); @@ -219,30 +216,42 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { meta: { lastScheduledActions: { date }, }, - }) => moment(date).utc() + }) => date ); + log.debug(`API RESULT: ${JSON.stringify(dateOnAllInstances)}`); + const instancesList = await pageObjects.alertDetailsUI.getAlertInstancesList(); expect(instancesList.map(instance => omit(instance, 'duration'))).to.eql([ { instance: 'us-central', status: 'Active', - start: dateOnAllInstances['us-central'].format('D MMM YYYY @ HH:mm:ss'), + start: moment(dateOnAllInstances['us-central']) + .utc() + .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-east', status: 'Active', - start: dateOnAllInstances['us-east'].format('D MMM YYYY @ HH:mm:ss'), + start: moment(dateOnAllInstances['us-east']) + .utc() + .format('D MMM YYYY @ HH:mm:ss'), }, { instance: 'us-west', status: 'Active', - start: dateOnAllInstances['us-west'].format('D MMM YYYY @ HH:mm:ss'), + start: moment(dateOnAllInstances['us-west']) + .utc() + .format('D MMM YYYY @ HH:mm:ss'), }, ]); + const durationEpoch = moment( + await pageObjects.alertDetailsUI.getAlertInstanceDurationEpoch() + ).utc(); + const durationFromInstanceTillPageLoad = mapValues(dateOnAllInstances, date => - moment.duration(testBeganAt.diff(moment(date).utc())) + moment.duration(durationEpoch.diff(moment(date).utc())) ); instancesList .map(alertInstance => ({ diff --git a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts index 900fe3237ffac..ddd88cb888534 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/alert_details.ts @@ -54,6 +54,12 @@ export function AlertDetailsPageProvider({ getService }: FtrProviderContext) { }; }); }, + async getAlertInstanceDurationEpoch(): Promise { + const alertInstancesDurationEpoch = await find.byCssSelector( + 'input[data-test-subj="alertInstancesDurationEpoch"]' + ); + return parseInt(await alertInstancesDurationEpoch.getAttribute('value'), 10); + }, async clickAlertInstanceMuteButton(instance: string) { const muteAlertInstanceButton = await testSubjects.find( `muteAlertInstanceButton_${instance}` From ce6029b9b0553ab66e6d1b151d21b56487972bd7 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 11 Mar 2020 09:37:03 +0100 Subject: [PATCH 02/13] [ML] Transforms: Use EuiInMemoryTable instead of custom typed table. (#59782) Before EuiInMemoryTable had TypeScript support we used our own typings and some state & CSS fixes. This is now all solved by the original EUI component. - Use EuiInMemoryTable instead of custom typed table. - Deletes some legacy leftover files. --- .github/CODEOWNERS | 1 - x-pack/index.js | 2 - x-pack/legacy/plugins/transform/index.ts | 12 -- .../plugins/transform/public/app/index.scss | 1 - .../components/transform_list/_index.scss | 1 - .../transform_list/_transform_table.scss | 48 -------- .../components/transform_list/columns.tsx | 28 ++--- .../transform_list/transform_list.tsx | 100 +++++++--------- .../transform_list/transform_table.tsx | 107 ------------------ .../transform/public/shared_imports.ts | 15 --- 10 files changed, 56 insertions(+), 259 deletions(-) delete mode 100644 x-pack/legacy/plugins/transform/index.ts delete mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_index.scss delete mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_transform_table.scss delete mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 552b2666eb3ea..a9af160d02084 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -88,7 +88,6 @@ /x-pack/test/functional/services/ml.ts @elastic/ml-ui # ML team owns the transform plugin, ES team added here for visibility # because the plugin lives in Kibana's Elasticsearch management section. -/x-pack/legacy/plugins/transform/ @elastic/ml-ui @elastic/es-ui /x-pack/plugins/transform/ @elastic/ml-ui @elastic/es-ui /x-pack/test/functional/apps/transform/ @elastic/ml-ui /x-pack/test/functional/services/transform_ui/ @elastic/ml-ui diff --git a/x-pack/index.js b/x-pack/index.js index 893802ea81621..c917befb4b3dd 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -31,7 +31,6 @@ import { crossClusterReplication } from './legacy/plugins/cross_cluster_replicat import { upgradeAssistant } from './legacy/plugins/upgrade_assistant'; import { uptime } from './legacy/plugins/uptime'; import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'; -import { transform } from './legacy/plugins/transform'; import { actions } from './legacy/plugins/actions'; import { alerting } from './legacy/plugins/alerting'; import { lens } from './legacy/plugins/lens'; @@ -61,7 +60,6 @@ module.exports = function(kibana) { infra(kibana), taskManager(kibana), rollup(kibana), - transform(kibana), siem(kibana), remoteClusters(kibana), crossClusterReplication(kibana), diff --git a/x-pack/legacy/plugins/transform/index.ts b/x-pack/legacy/plugins/transform/index.ts deleted file mode 100644 index a4b980c0bf8f3..0000000000000 --- a/x-pack/legacy/plugins/transform/index.ts +++ /dev/null @@ -1,12 +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 function transform(kibana: any) { - return new kibana.Plugin({ - id: 'transform', - configPrefix: 'xpack.transform', - }); -} diff --git a/x-pack/plugins/transform/public/app/index.scss b/x-pack/plugins/transform/public/app/index.scss index 836929174875e..beb5ee6be67e6 100644 --- a/x-pack/plugins/transform/public/app/index.scss +++ b/x-pack/plugins/transform/public/app/index.scss @@ -15,4 +15,3 @@ @import 'sections/create_transform/components/wizard/index'; @import 'sections/transform_management/components/create_transform_button/index'; @import 'sections/transform_management/components/stats_bar/index'; -@import 'sections/transform_management/components/transform_list/index'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_index.scss b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_index.scss deleted file mode 100644 index acb4bd0cf4326..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'transform_table'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_transform_table.scss b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_transform_table.scss deleted file mode 100644 index a9e2e4d790436..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/_transform_table.scss +++ /dev/null @@ -1,48 +0,0 @@ -.transform__TransformTable { - // Using an override as a last resort because we cannot set custom classes on - // nested upstream components. The opening animation limits the height - // of the expanded row to 1000px which turned out to be not predictable. - // The animation could also result in flickering with expanded rows - // where the inner content would result in the DOM changing the height. - .euiTableRow-isExpandedRow .euiTableCellContent { - animation: none !important; - .euiTableCellContent__text { - width: 100%; - } - } - // Another override: Because an update to the table replaces the DOM, the same - // icon would still again fade in with an animation. If the table refreshes with - // e.g. 1s this would result in a blinking icon effect. - .euiIcon-isLoaded { - animation: none !important; - } -} -.transform__BulkActionItem { - display: block; - padding: $euiSizeS; - width: 100%; - text-align: left; -} - -.transform__BulkActionsBorder { - height: 20px; - border-right: $euiBorderThin; - width: 1px; - display: inline-block; - vertical-align: middle; - height: 35px; - margin: 0px 5px; - margin-top: -5px; -} - -.transform__ProgressBar { - margin-bottom: $euiSizeM; -} - -.transform__TaskStateBadge, .transform__TaskModeBadge { - max-width: 100px; -} - -.transform__TransformTable__messagesPaneTable .euiTableCellContent__text { - text-align: left; -} diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx index fb24ff2a12e02..159833354b5ef 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/columns.tsx @@ -9,6 +9,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiBadge, + EuiTableActionsColumnType, + EuiTableComputedColumnType, + EuiTableFieldDataColumnType, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, @@ -21,13 +24,6 @@ import { import { TransformId, TRANSFORM_STATE } from '../../../../../../common'; -import { - ActionsColumnType, - ComputedColumnType, - ExpanderColumnType, - FieldDataColumnType, -} from '../../../../../shared_imports'; - import { getTransformProgress, TransformListRow, @@ -89,15 +85,15 @@ export const getColumns = ( } const columns: [ - ExpanderColumnType, - FieldDataColumnType, - FieldDataColumnType, - FieldDataColumnType, - FieldDataColumnType, - ComputedColumnType, - ComputedColumnType, - ComputedColumnType, - ActionsColumnType + EuiTableComputedColumnType, + EuiTableFieldDataColumnType, + EuiTableFieldDataColumnType, + EuiTableFieldDataColumnType, + EuiTableFieldDataColumnType, + EuiTableComputedColumnType, + EuiTableComputedColumnType, + EuiTableComputedColumnType, + EuiTableActionsColumnType ] = [ { name: ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index 9c2da53c36d6b..3393aada8b69d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, MouseEventHandler, FC, useContext, useState } from 'react'; +import React, { MouseEventHandler, FC, useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { + Direction, EuiBadge, EuiButtonEmpty, EuiButtonIcon, @@ -16,15 +17,13 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, + EuiInMemoryTable, EuiPopover, EuiTitle, - Direction, } from '@elastic/eui'; import { TransformId, TRANSFORM_STATE } from '../../../../../../common'; -import { OnTableChangeArg, SortDirection, SORT_DIRECTION } from '../../../../../shared_imports'; - import { useRefreshTransformList, TransformListRow, @@ -43,7 +42,6 @@ import { StopAction } from './action_stop'; import { ItemIdToExpandedRowMap, Query, Clause } from './common'; import { getColumns } from './columns'; import { ExpandedRow } from './expanded_row'; -import { ProgressBar, transformTableFactory } from './transform_table'; function getItemIdToExpandedRowMap( itemIds: TransformId[], @@ -74,8 +72,6 @@ interface Props { transformsLoading: boolean; } -const TransformTable = transformTableFactory(); - export const TransformList: FC = ({ errorMessage, isInitialized, @@ -100,7 +96,7 @@ export const TransformList: FC = ({ const [pageSize, setPageSize] = useState(10); const [sortField, setSortField] = useState(TRANSFORM_LIST_COLUMN.ID); - const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); + const [sortDirection, setSortDirection] = useState('asc'); const { capabilities } = useContext(AuthorizationContext); const disabled = @@ -186,52 +182,46 @@ export const TransformList: FC = ({ // Before the transforms have been loaded for the first time, display the loading indicator only. // Otherwise a user would see 'No transforms found' during the initial loading. if (!isInitialized) { - return ; + return null; } if (typeof errorMessage !== 'undefined') { return ( - - - -
{JSON.stringify(errorMessage)}
-
-
+ +
{JSON.stringify(errorMessage)}
+
); } if (transforms.length === 0) { return ( - - - - {i18n.translate('xpack.transform.list.emptyPromptTitle', { - defaultMessage: 'No transforms found', - })} -

- } - actions={[ - - {i18n.translate('xpack.transform.list.emptyPromptButtonText', { - defaultMessage: 'Create your first transform', - })} - , - ]} - data-test-subj="transformNoTransformsFound" - /> - + + {i18n.translate('xpack.transform.list.emptyPromptTitle', { + defaultMessage: 'No transforms found', + })} +

+ } + actions={[ + + {i18n.translate('xpack.transform.list.emptyPromptButtonText', { + defaultMessage: 'Create your first transform', + })} + , + ]} + data-test-subj="transformNoTransformsFound" + /> ); } @@ -362,15 +352,15 @@ export const TransformList: FC = ({ const onTableChange = ({ page = { index: 0, size: 10 }, - sort = { field: TRANSFORM_LIST_COLUMN.ID, direction: SORT_DIRECTION.ASC }, - }: OnTableChangeArg) => { + sort = { field: TRANSFORM_LIST_COLUMN.ID as string, direction: 'asc' }, + }) => { const { index, size } = page; setPageIndex(index); setPageSize(size); const { field, direction } = sort; - setSortField(field); - setSortDirection(direction); + setSortField(field as string); + setSortDirection(direction as Direction); }; const selection = { @@ -379,8 +369,7 @@ export const TransformList: FC = ({ return (
- - = ({ items={filterActive ? filteredTransforms : transforms} itemId={TRANSFORM_LIST_COLUMN.ID} itemIdToExpandedRowMap={itemIdToExpandedRowMap} + loading={isLoading || transformsLoading} onTableChange={onTableChange} pagination={pagination} rowProps={item => ({ @@ -399,11 +389,9 @@ export const TransformList: FC = ({ selection={selection} sorting={sorting} search={search} - data-test-subj={ - isLoading || transformsLoading - ? 'transformListTable loading' - : 'transformListTable loaded' - } + data-test-subj={`transformListTable ${ + isLoading || transformsLoading ? 'loading' : 'loaded' + }`} />
); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx deleted file mode 100644 index 8c7920c124bef..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_table.tsx +++ /dev/null @@ -1,107 +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. - */ - -// This component extends EuiInMemoryTable with some -// fixes and TS specs until the changes become available upstream. - -import React, { Fragment } from 'react'; - -import { EuiProgress } from '@elastic/eui'; - -import { mlInMemoryTableBasicFactory } from '../../../../../shared_imports'; - -// The built in loading progress bar of EuiInMemoryTable causes a full DOM replacement -// of the table and doesn't play well with auto-refreshing. That's why we're displaying -// our own progress bar on top of the table. `EuiProgress` after `isLoading` displays -// the loading indicator. The variation after `!isLoading` displays an empty progress -// bar fixed to 0%. Without it, the display would vertically jump when showing/hiding -// the progress bar. -export const ProgressBar = ({ isLoading = false }) => { - return ( - - {isLoading && } - {!isLoading && ( - - )} - - ); -}; - -// copied from EUI to be available to the extended getDerivedStateFromProps() -function findColumnByProp(columns: any, prop: any, value: any) { - for (let i = 0; i < columns.length; i++) { - const column = columns[i]; - if (column[prop] === value) { - return column; - } - } -} - -// copied from EUI to be available to the extended getDerivedStateFromProps() -const getInitialSorting = (columns: any, sorting: any) => { - if (!sorting || !sorting.sort) { - return { - sortName: undefined, - sortDirection: undefined, - }; - } - - const { field: sortable, direction: sortDirection } = sorting.sort; - - // sortable could be a column's `field` or its `name` - // for backwards compatibility `field` must be checked first - let sortColumn = findColumnByProp(columns, 'field', sortable); - if (sortColumn == null) { - sortColumn = findColumnByProp(columns, 'name', sortable); - } - - if (sortColumn == null) { - return { - sortName: undefined, - sortDirection: undefined, - }; - } - - const sortName = sortColumn.name; - - return { - sortName, - sortDirection, - }; -}; - -export function transformTableFactory() { - const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); - return class TransformTable extends MlInMemoryTableBasic { - static getDerivedStateFromProps(nextProps: any, prevState: any) { - const derivedState = { - ...prevState.prevProps, - pageIndex: nextProps.pagination.initialPageIndex, - pageSize: nextProps.pagination.initialPageSize, - }; - - if (nextProps.items !== prevState.prevProps.items) { - Object.assign(derivedState, { - prevProps: { - items: nextProps.items, - }, - }); - } - - const { sortName, sortDirection } = getInitialSorting(nextProps.columns, nextProps.sorting); - if ( - sortName !== prevState.prevProps.sortName || - sortDirection !== prevState.prevProps.sortDirection - ) { - Object.assign(derivedState, { - sortName, - sortDirection, - }); - } - return derivedState; - } - }; -} diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 3582dd5d266e2..4def1bc98ef8c 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -24,21 +24,6 @@ export { DAY, } from '../../../../src/plugins/es_ui_shared/public/components/cron_editor'; -// Custom version of EuiInMemoryTable with TypeScript -// support and a fix for updating sorting props. -export { - ActionsColumnType, - ComputedColumnType, - ExpanderColumnType, - FieldDataColumnType, - ColumnType, - mlInMemoryTableBasicFactory, - OnTableChangeArg, - SortingPropType, - SortDirection, - SORT_DIRECTION, -} from '../../../legacy/plugins/ml/public/application/components/ml_in_memory_table'; - // Needs to be imported because we're reusing KqlFilterBar which depends on it. export { setDependencyCache } from '../../../legacy/plugins/ml/public/application/util/dependency_cache'; From babf81bdc02174145f0416e579f8e18a8d27c2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Wed, 11 Mar 2020 09:36:12 +0000 Subject: [PATCH 03/13] [RFC] Pulse (#57108) * [RFC][skip-ci] Pulse * Add drawback * Add Opt-In/Out endpoint * Add clarification about synched local internal indices * Update rfcs/text/0008_pulse.md Co-Authored-By: Josh Dover * Add Phased implementation intentions, Security and Integrity challenges and example of use * Refer to a follow up RFC to talk about security in the future * Fix wording + add Legacy behaviour Co-authored-by: Elastic Machine Co-authored-by: Josh Dover --- rfcs/images/pulse_diagram.png | Bin 0 -> 167068 bytes rfcs/text/0008_pulse.md | 316 ++++++++++++++++++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 rfcs/images/pulse_diagram.png create mode 100644 rfcs/text/0008_pulse.md diff --git a/rfcs/images/pulse_diagram.png b/rfcs/images/pulse_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..a104fad0fe133c25d9efecc87b99bc448f0e6061 GIT binary patch literal 167068 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 EAM>lQ{Qv*} literal 0 HcmV?d00001 diff --git a/rfcs/text/0008_pulse.md b/rfcs/text/0008_pulse.md new file mode 100644 index 0000000000000..e2543d310aa38 --- /dev/null +++ b/rfcs/text/0008_pulse.md @@ -0,0 +1,316 @@ +- Start Date: 2020-02-07 +- RFC PR: [#57108](https://github.com/elastic/kibana/pull/57108) +- Kibana Issue: (leave this empty) + +# Table of contents + +- [Summary](#summary) +- [Motivation](#motivation) +- [Detailed design](#detailed-design) + - [Concepts](#concepts) + - [Architecture](#architecture) + 1. [Remote Pulse Service](#1-remote-pulse-service) + - [Deployment](#deployment) + - [Endpoints](#endpoints) + - [Authenticate](#authenticate) + - [Opt-In|Out](#opt-inout) + - [Inject telemetry](#inject-telemetry) + - [Retrieve instructions](#retrieve-instructions) + - [Data model](#data-model) + - [Access Control](#access-control) + 2. [Local Pulse Service](#2-local-pulse-service) + - [Data storage](#data-storage) + - [Sending telemetry](#sending-telemetry) + - [Instruction polling](#instruction-polling) +- [Drawbacks](#drawbacks) +- [Alternatives](#alternatives) +- [Adoption strategy](#adoption-strategy) +- [How we teach this](#how-we-teach-this) +- [Unresolved questions](#unresolved-questions) + +# Summary + +Evolve our telemetry to collect more diverse data, enhance our products with that data and engage with users by enabling: + +1. _Two-way_ communication link between us and our products. +2. Flexibility to collect diverse data and different granularity based on the type of data. +3. Enhanced features in our products, allowing remote-driven _small tweaks_ to existing builds. +4. All this while still maintaining transparency about what we send and making sure we don't track any of the user's data. + +# Basic example + +There is a POC implemented in the branch [`pulse_poc`](https://github.com/elastic/kibana/tree/pulse_poc) in this repo. + +It covers the following scenarios: + +- Track the behaviour of our users in the UI, reporting UI events throughout our platform. +- Report to Elastic when an unexpected error occurs and keep track of it. When it's fixed, it lets the user know, encouraging them to update to their deployment to the latest release (PR [#56724](https://github.com/elastic/kibana/pull/56724)). +- Keep track of the notifications and news in the newsfeed to know when they are read/kept unseen. This might help us on improving the way we communicate updates to the user (PR [#53596](https://github.com/elastic/kibana/pull/53596)). +- Provide a cost estimate for running that cluster in Elastic Cloud, so the user is well-informed about our up-to-date offering and can decide accordingly (PR [#56324](https://github.com/elastic/kibana/pull/56324)). +- Customised "upgrade guide" from your current version to the latest (PR [#56556](https://github.com/elastic/kibana/pull/56556)). + +![image](../images/pulse_diagram.png) +_Basic example of the architecture_ + +# Motivation + +Based on our current telemetry, we have many _lessons learned_ we want to tackle: + +- It only supports one type of data: + - It makes simple tasks like reporting aggregations of usage based on a number of days [an overengineered solution](https://github.com/elastic/kibana/issues/46599#issuecomment-545024137) + - When reporting arrays (i.e.: `ui_metrics`), it cannot be consumed, making the data useless. +- _One index to rule them all_: +The current unique document structure comes at a price: + - People consuming that information finding it hard to understand each element in the document ([[DISCUSS] Data dictionary for product usage data](https://github.com/elastic/telemetry/issues/211)) + - Maintaining the mappings is a tedious and risky process. It involved increasing the setting for the limit of fields in a mapping and reindexing documents (now millions of them). + - We cannot fully control the data we insert in the documents: If we set `mappings.dynamic: 'strict'`, we'll reject all the documents containing more information than the actually mapped, losing all the other content we do want to receive. +- Opt-out ratio: +We want to reduce the number of `opt-out`s by providing some valuable feedback to our users so that they want to turn telemetry ON because they do benefit from it. + +# Detailed design + +This design is going to be tackled by introducing some common concepts to be used by the main two main components in this architecture: + +1. Remote Pulse Service (RPS) +2. Local Pulse Service (LPS) + +After that, it explains how we envision the architecture and design of each of those components. + +## Concepts + +There are some new concepts we'd like to introduce with this new way of reporting telemetry: + +- **Deployment Hash ID** +This is the _anonymised_ random ID assigned for a deployment. It is used to link multiple pieces of information for further analysis like cross-referencing different bits of information from different sources. +- **Channels** +This is each stream of data that have common information. Typically each channel will have a well defined source of information, different to the rest. It will also result in a different structure to the rest of channels. However, all the channels will maintain a minimum piece of common schema for cross-references (like **Deployment Hash ID** and **timestamp**). +- **Instructions** +These are the messages generated in the form of feedback to the different channels. +Typically, channels will follow a bi-directional communication process _(Local <-> Remote)_ but there might be channels that do not generate any kind of instruction _(Local -> Remote)_ and, similarly, some other channels that do not provide any telemetry at all, but allows Pulse to send updates to our products _(Local <- Remote)_. + +## Phased implementation + +At the moment of writing this document, anyone can push _fake_ telemetry data to our Telemetry cluster. They only need to know the public encryption key, the endpoint and the format of the data, all of that easily retrievable. We take that into consideration when analysing the data we have at the moment and it is a risk we are OK with for now. + +But, given that we aim to provide feedback to the users and clusters in the form of instructions, the **Security and Integrity of the information** is critical. We need to come up with a solution that ensures the instructions are created based on data that was uniquely created (signed?) by the source. If we cannot ensure that, we should not allow that piece of information to be used in the generation of the instructions for that cluster and we should mark it so we know it could be maliciously injected when using it in our analysis. + +But also, we want to be able to ship the benefits of Pulse on every release. That's why we are thinking on a phased release, starting with limited functionality and evolving to the final complete vision of this product. This RFC suggests the following phased implementation: + +1. **Be able to ingest granular data** +With the introduction of the **channels**, we can start receiving granular data that will help us all on our analysis. At this point, the same _security_ features as the current telemetry are considered: The payload is encrypted by the Kibana server so no mediator can spoof the data. +The same risks as the current telemetry still apply at this point: anyone can _impersonate_ and send the data on behalf of another cluster, making the collected information useless. +Because this information cannot be used to generate any instruction, we may not care about the **Deployment Hash ID** at this stage. This means no authentication is required to push data. +The works at this point in time will be focused on creating the initial infraestructure, receiving early data and start with the migration of the current telemetry into the new channel-based model. Finally, start exploring the new visualisations we can provide with this new model of data. + +2. **Secured ingest channel** +In this phase, our efforts will focus on securing the communications and integrity of the data. This includes: + - **Generation of the Deployment Hash ID**: + Discussions on whether it should be self-generated and accepted/rejected by the Remote Pulse Service (RPS) or it should be generated and assigned by the RPS because it is the only one that can ensure uniqueness. + - **Locally store the Deployment Hash ID as an encrypted saved object**: + This comes back with a caveat: OSS versions will not be able to receive instructions. We will need to maintain a fallback mechanism to the phase 1 logic (it may be a desired scenario because it could happen the encrypted saved objects are not recoverable due to an error in the deployment and we should still be able to apply that fallback). + - **Authenticity of the information (Local -> Remote)**: + We need to _sign_ the data in some way the RPS can confirm the information reported as for a _Deployment Hash ID_ comes from the right source. + - **Authenticity of the information (Remote -> Local)**: + We need the Local Pulse Service (LPS) to be able to confirm the responses from the RPS data has not been altered by any mediator. It could be done via encryption using a key provided by the LPS. This should be provided to the RPS inside an encrypted payload in the same fashion we currently encrypt the telemetry. + - **Integrity of the data in the channels**: + We need to ensure an external plugin cannot push data to channels to avoid malicious corruption of the data. We could achieve this by either making this plugin only available to Kibana-shipped plugins or storing the `pluginID` that is pushing the data to have better control of the source of the data (then an ingest pipeline can reject any source of data that should not be accepted). + + All the suggestions in this phase can be further discussed at that point (I will create another RFC to discuss those terms after this RFC is approved and merged). + +3. **Instruction handling** +This final phase we'll implement the instruction generation and handling at the same time we are adding more **channels**. +We can discuss at this point if we want to be able to provide _harmless_ instructions for those deployments that are not _secured_ (i.e.: Cloud cost estimations, User-profiled-based marketing updates, ...). + +## Architecture + +As mentioned earlier, at the beginning of this chapter, there are two main components in this architecture: + +1. Remote Pulse Service +2. Local Pulse Service + +### 1. Remote Pulse Service + +This is the service that will receive and store the telemetry from all the _opted-in_ deployments. It will also generate the messages we want to report back to each deployment (aka: instructions). + +#### Deployment + +- The service will be hosted by Elastic. +- Most likely maintained by the Infra team. +- GCP is contemplated at this moment, but we need to confirm how would it affect us regarding the FedRamp approvals (and similar). +- Exposes an API (check [Endpoints](#endpoints) to know more) to inject the data and retrieve the _instructions_. +- The data will be stored in an ES cluster. + +#### Endpoints + +The following endpoints **will send every payload** detailed in below **encrypted** with a similar mechanism to the current telemetry encryption. + +##### Authenticate + +This Endpoint will be used to retrieve a randomised `deploymentID` and a `token` for the cluster to use in all the subsequent requests. Ideally, it will provide some sort of identifier (like `cluster_uuid` or `license.uuid`) so we can revoke its access to any of the endpoints if explicitly requested ([Blocking telemetry input](https://github.com/elastic/telemetry/pull/221) and [Delete previous telemetry data](https://github.com/elastic/telemetry/issues/209)). + +I'd appreciate some insights here to come up with a strong handshake mechanism to avoid stealing identities. + +In order to _dereference_ the data, we can store these mappings in a Vault or Secrets provider instead of an index in our ES. + +_NB: Not for phase 1_ + +##### Opt-In|Out + +Similar to the current telemetry, we want to keep track of when the user opts in or out of telemetry. The implementation can be very similar to the current one. But we recently learned we need to add the origin to know what application has telemetry disabled (Kibana, Beats, Enterprise Search, ...). This makes me wonder if we will ever want to provide a granular option for the user to be able to cherry-pick about what channels are sent and which ones should be disabled. + +##### Inject telemetry + +In order to minimise the amount of requests, this `POST` should accept bulks of data in the payload (mind the payload size limits if any). It will require authentication based on the `deploymentID` and `token` explained in the [previous endpoint](#authenticate) (_NB: Not for phase 1_). + +The received payload will be pushed to a streaming technology (AWS Firehose, Google Pub/Sub, ...). This way we can maintain a buffer in cases the ingestion of data spikes or we need to stop our ES cluster for any maintenance purposes. + +A subscriber to that stream will receive that info a split the payload into smaller documents per channel and index them into their separate indices. + +This indexing should also trigger some additional processes like the **generation of instructions** and _special views_ (only if needed, check the point [Access control](#access-control) for more details). + +_NB: We might want to consider some sort of piggy-backing to include the instructions in the response. But for the purpose of this RFC, scalability and separation of concerns, I'd rather keep it for future possible improvements._ + +##### Retrieve instructions + +_NB: Only after phase 3_ + +This `GET` endpoint should return the list of instructions generated for that deployment. To control the likely ever-growing list of instructions for each deployment, it will accept a `since` query parameter where the requester can specify the timestamp ever since it was to retrieve the new values. + +This endpoint will read the `instructions-*` indices, filtering `updated-at` by the `since` query parameter (if provided) and it will return the results, grouping them by channels. + +Additionally, we can consider accepting an additional query parameter to retrieve only specific channels. For use cases like distributed components (endpoint, apm, beats, ...) polling instructions themselves. + +#### Data model + +The storage of each of the documents, will be based on monthly-rolling indices split by channels. This means we'll have indices like `pulse-raw-{CHANNEL_NAME}-YYYY.MM` and `pulse-instructions-{CHANNEL_NAME}-YYYY.MM` (final names TBD). + +The first group will be used to index all the incoming documents from the telemetry. While the second one will contain the instructions to be sent to the deployments. + +The mapping for those indices will be **`strict`** to avoid anyone storing unwanted/not-allowed info. The indexer defined in [the _Inject telemetry_ endpoint](#inject-telemetry) will need to handle accordingly the errors derived from the strict mapping. +We'll set up a process to add new mappings and their descriptions before every new release. + +#### Access control + +- The access to _raw_ data indices will be very limited. Only granted to those in need of troubleshooting the service and maintaining mappings (this is the Pulse/Telemetry team at the moment). +- Special views (as in aggregations/visualisations/snapshots of the data stored in special indices via separated indexers/aggregators/ES transform or via _BigQuery_ or similar) will be defined for different roles in the company to help them to take informed decisions based on the data. +This way we'll be able to control "who can see what" on a very granual basis. It will also provide us with more flexibility to change to structure of the _raw_ if needed. + +### 2. Local Pulse Service + +This refers to the plugin running in Kibana in each of our customers' deployments. It will be a core service in NP, available for all plugins to get the existing channels, to send pieces of data, and subscribe to instructions. + +The channel handlers are only defined inside the pulse context and are used to normalise the data for each channel before sending it to the remote service. The CODEOWNERS should notify the Pulse team every time there's an intended change in this context. + +#### Data storage + +For the purpose of transparency, we want the user to be able to retrieve the telemetry we send at any point, so we should store the information we send for each channel in their own local _dot_ internal indices (similar to a copy of the `pulse-raw-*` and `pulse-instructions-*` indices in our remote service). We may want to also sync back from the remote service any updates we do to the documents: enrichment of the document, anonymisation, categorisation when it makes sense in that specific channel, ... + +In the same effort, we could even provide some _dashboards_ in Kibana for specific roles in the cluster to understand more about their deployment. + +Only those specific roles (admin?) should have access to these local indices, unless they grant permissions to other users they want to share this information with. + +The users should be able to control how long they want to keep that information for via ILM. A default ILM policy will be setup during the startup if it doesn't exist. + +#### Sending telemetry + +The telemetry will be sent, preferably, from the server. Only falling back to the browser in case we detect the server is behind firewalls and it cannot reach the service or if the user explicitly sets the behaviour in the config. + +Periodically, the process (either in the server or the browser) will retrieve the telemetry to be sent by the channels, compile it into 1 bulk payload and send it encrypted to the [ingest endpoint](#inject-telemetry) explained earlier. + +How often it sends the data, depends on the channel specifications. We will have 3 levels of periodicity: + +- `URGENT`: The data is sent as soon as possible. +- `HIGH`: Sent every hour. +- `NORMAL`: Sent every 24 hours. +- `LOW`: Sent every 3 days. + +Some throttling policy should be applied to avoid exploiting the exceeded use of `URGENT`. + +#### Instruction polling + +Similarly to the sending of the telemetry, the instruction polling should happen only on one end (either the server or the browser). It will store the responses in the local index for each channel and the plugins reacting to those instructions will be able to consume that information based on their own needs (either load only the new ones or all the historic data at once). + +Depending on the subscriptions to the channels by the plugins, the polling will happen with different periodicity, similar to the one described in the chapter above. + +#### Exposing channels to the plugins + +The plugins will be able to send messages and/or consume instructions for any channel by using the methods provided as part of the `coreContext` in the `setup` and `start` lifecycle methods in a fashion like (types to be properly defined when implementing it): + +```typescript +const coreContext: CoreSetup | CoreStart = { + ...existingCoreContext, + pulse: { + sendToChannel: async (channelName: keyof Channels, payload: Channels[channelName]) => void, + instructionsFromChannel$: (channelName: keyof Channels) => Observable, + }, +} +``` + +Plugins will simply need to call `core.pulse.sendToChannel('errors', myUnexpectedErrorIWantToReport)` whenever they want to report any new data to that channel. This will call the channel's handler to store the data. + +Similarly, they'll be able to subscribe to channels like: + +```typescript +core.pulse.instructionsFromChannel$('ui_behaviour_tracking') + .pipe(filterInstructionsForMyPlugin) // Initially, we won't filter the instructions based on the plugin ID (might not be necessary in all cases) + .subscribe(changeTheOrderOfTheComponents); +``` + +Internally in those methods we should append the `pluginId` to know who is sending/receiving the info. + +##### The _legacy_ collection + +The current telemetry collection via the `UsageCollector` service will be maintained until all the current telemetry is fully migrated into their own channels. In the meantime, the current existing telemetry will be sent to Pulse as the `legacy` channel. This way we can maintain the same architecture for the old and new telemetry to come. At this stage, there is no need for any plugin to update their logic unless they want to send more granular data using other (even specific to that plugin) channels. + +The mapping for this `legacy` channel will be kept `dynamic: false` instead of `strict` to ensure compatibility. + +# Drawbacks + +- Pushing data into telemetry nowadays is as simple as implementing your own `usageCollector`. For consuming, though, the telemetry team needs to update the mappings. But as soon as they do so, the previous data is available. Now we'll be more strict about the mapping. Rejecting any data that does not comply. Changing the structure of the reported data will result in data loss in that channel. +- Hard dependency on the Pulse team's availability to update the metrics and on the Infra team to deploy the instruction handlers. +- Testing architecture: any dockerised way to test the local dev environment? +- We'll increase the local usage of indices. Making it more expensive to users to maintain the cluster. We need be to careful with this! Although it might not change much, compared to the current implementation, if any plugin decides to maintain its own index/saved objects to do aggregations afterwards. Similarly, more granularity per channel, may involve more network usage. +- It is indeed a breaking change, but it can be migrated over-time as new features, making use of the instructions. +- We need to update other products already reporting telemetry from outside Kibana (like Beats, Enterprise Search, Logstash, ...) to use the new way of pushing telemetry. + +# Alternatives + +> What other designs have been considered? + +We currently have the newsfeed to be able to communicate to the user. This is actually pulling in Kibana from a public API to retrieve the list of entries to be shown in the notification bar. But this is only limitted to notifications to the user while the new _intructions_ can provide capabilities like self-update/self-configuration of components like endpoints, elasticsearch, ... + +> What is the impact of not doing this? + +Users might not see any benefit from providing telemetry and will opt-out. The quality of the telemetry will likely not be as good (or it will require a higher effort on the plugin end to provide it like in [the latest lens effort](https://github.com/elastic/kibana/issues/46599#issuecomment-545024137)) + +# Adoption strategy + +Initially, we'll focus on the remote service and move the current telemetry to report as a `"legacy"` channel to the new Pulse service. + +Then, we'll focus on doing the client side, providing new APIs to report the data, aiming for the minimum changes on the public end. For instance, the current usage collectors already report an ID, we can work on those IDs mapping to a channel (only grouping them when it makes sense). Nevertheless, it will require the devs to engage with the Pulse team for the mappings and definitions to be properly set up and updated. And any views to be added. + +Finally, the instruction handling APIs are completely new and it will require development on both _remote_ and _local_ ends for the instruction generation and handling. + +# How we teach this + +> What names and terminology work best for these concepts and why? How is this +idea best presented? As a continuation of existing Kibana patterns? + +We have 3 points of view to show here: + +- From the users perspective, we need to show the value for them to have the telemetry activated. +- From the devs, how to generate data and consume instructions. +- From the PMs, how to consume the views + definitions of the fields. + +> Would the acceptance of this proposal mean the Kibana documentation must be +re-organized or altered? Does it change how Kibana is taught to new developers +at any level? + +This telemetry is supposed to be internal only. Only internal developers will be able to add to this. So the documentation will only be for internal puposes. As mentioned in the _Adoption strategy_, the idea is that the devs to report new data to telemetry will need to engage with the Pulse team. + +> How should this feature be taught to existing Kibana developers? + +# Unresolved questions + +- Pending to define a proper handshake in the authentication mechanism to reduce the chance of a man-in-the-middle attack or DDoS. => We already have some ideas thanks to @jportner and @kobelb but it will be resolved during the _Phase 2_ design. +- Opt-in/out per channel? From bd3f94e458b56ced26af236d0e62c307e60423eb Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 11 Mar 2020 16:31:19 +0300 Subject: [PATCH 04/13] Vislib legend toggle broken (#59736) Closes: #59254 --- .../public/persisted_state/persisted_state.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/plugins/visualizations/public/persisted_state/persisted_state.ts b/src/plugins/visualizations/public/persisted_state/persisted_state.ts index d09dcd5381511..b81b651c73509 100644 --- a/src/plugins/visualizations/public/persisted_state/persisted_state.ts +++ b/src/plugins/visualizations/public/persisted_state/persisted_state.ts @@ -26,10 +26,8 @@ function prepSetParams(key: PersistedStateKey, value: any, path: PersistedStateP // key must be the value, set the entire state using it if (value === undefined && (isPlainObject(key) || path.length > 0)) { // setting entire tree, swap the key and value to write to the state - return { - value: key, - key: undefined, - }; + value = key; + key = undefined; } // ensure the value being passed in is never mutated From db2a92c61de757fc5960a9f49cbfadcebe08f827 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Wed, 11 Mar 2020 08:44:29 -0500 Subject: [PATCH 05/13] [Metrics Alerts] Fix error when a metric reports no data (#59810) * [Metrics Alerts] Fix error when a metric reports no data * Clarify no data case handler, add separate error state * Throw error state when callCluster fails --- .../register_metric_threshold_alert_type.ts | 89 +++++++++++-------- .../lib/alerting/metric_threshold/types.ts | 2 + 2 files changed, 56 insertions(+), 35 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 399f09bd3e776..d318171f3bb48 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -37,9 +37,14 @@ const FIRED_ACTIONS = { }; const getCurrentValueFromAggregations = (aggregations: Aggregation) => { - const { buckets } = aggregations.aggregatedIntervals; - const { value } = buckets[buckets.length - 1].aggregatedValue; - return value; + try { + const { buckets } = aggregations.aggregatedIntervals; + if (!buckets.length) return null; // No Data state + const { value } = buckets[buckets.length - 1].aggregatedValue; + return value; + } catch (e) { + return undefined; // Error state + } }; const getParsedFilterQuery: ( @@ -138,34 +143,37 @@ const getMetric: ( aggs, }; - if (groupBy) { - const bucketSelector = ( - response: InfraDatabaseSearchResponse<{}, CompositeAggregationsResponse> - ) => response.aggregations?.groupings?.buckets || []; - const afterKeyHandler = createAfterKeyHandler( - 'aggs.groupings.composite.after', - response => response.aggregations?.groupings?.after_key - ); - const compositeBuckets = (await getAllCompositeData( - body => callCluster('search', { body, index: indexPattern }), - searchBody, - bucketSelector, - afterKeyHandler - )) as Array; - return compositeBuckets.reduce( - (result, bucket) => ({ - ...result, - [bucket.key.groupBy]: getCurrentValueFromAggregations(bucket), - }), - {} - ); + try { + if (groupBy) { + const bucketSelector = ( + response: InfraDatabaseSearchResponse<{}, CompositeAggregationsResponse> + ) => response.aggregations?.groupings?.buckets || []; + const afterKeyHandler = createAfterKeyHandler( + 'aggs.groupings.composite.after', + response => response.aggregations?.groupings?.after_key + ); + const compositeBuckets = (await getAllCompositeData( + body => callCluster('search', { body, index: indexPattern }), + searchBody, + bucketSelector, + afterKeyHandler + )) as Array; + return compositeBuckets.reduce( + (result, bucket) => ({ + ...result, + [bucket.key.groupBy]: getCurrentValueFromAggregations(bucket), + }), + {} + ); + } + const result = await callCluster('search', { + body: searchBody, + index: indexPattern, + }); + return { '*': getCurrentValueFromAggregations(result.aggregations) }; + } catch (e) { + return { '*': undefined }; // Trigger an Error state } - - const result = await callCluster('search', { - body: searchBody, - index: indexPattern, - }); - return { '*': getCurrentValueFromAggregations(result.aggregations) }; }; const comparatorMap = { @@ -220,14 +228,15 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet criteria.map(criterion => (async () => { const currentValues = await getMetric(services, criterion, groupBy, filterQuery); - if (typeof currentValues === 'undefined') - throw new Error('Could not get current value of metric'); const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; return mapValues(currentValues, value => ({ - shouldFire: comparisonFunction(value, threshold), + shouldFire: + value !== undefined && value !== null && comparisonFunction(value, threshold), currentValue: value, + isNoData: value === null, + isError: value === undefined, })); })() ) @@ -237,8 +246,12 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet for (const group of groups) { const alertInstance = services.alertInstanceFactory(`${alertUUID}-${group}`); + // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every(result => result[group].shouldFire); - + // AND logic; because we need to evaluate all criteria, if one of them reports no data then the + // whole alert is in a No Data/Error state + const isNoData = alertResults.some(result => result[group].isNoData); + const isError = alertResults.some(result => result[group].isError); if (shouldAlertFire) { alertInstance.scheduleActions(FIRED_ACTIONS.id, { group, @@ -248,7 +261,13 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet // Future use: ability to fetch display current alert state alertInstance.replaceState({ - alertState: shouldAlertFire ? AlertStates.ALERT : AlertStates.OK, + alertState: isError + ? AlertStates.ERROR + : isNoData + ? AlertStates.NO_DATA + : shouldAlertFire + ? AlertStates.ALERT + : AlertStates.OK, }); } }, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index 1c3d0cea3dc84..e247eb8a3f889 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -19,6 +19,8 @@ export enum Comparator { export enum AlertStates { OK, ALERT, + NO_DATA, + ERROR, } export type TimeUnit = 's' | 'm' | 'h' | 'd'; From 2f912a366144ae498b9cdc864db3757d7f832d79 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 11 Mar 2020 08:53:20 -0500 Subject: [PATCH 06/13] [DOCS] Clarification in tutorial (#59088) * Cl[DOCS] arification in tutorial * Review comments --- .../tutorial-discovering.asciidoc | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/getting-started/tutorial-discovering.asciidoc b/docs/getting-started/tutorial-discovering.asciidoc index bbffb2187f0cf..355477286d445 100644 --- a/docs/getting-started/tutorial-discovering.asciidoc +++ b/docs/getting-started/tutorial-discovering.asciidoc @@ -1,20 +1,19 @@ [[tutorial-discovering]] === Discover your data -Using *Discover*, you can enter +Using *Discover*, enter an {ref}/query-dsl-query-string-query.html#query-string-syntax[Elasticsearch query] to search your data and filter the results. . Open *Discover*. + -The current index pattern appears below the filter bar, in this case `shakes*`. -You might need to click *New* in the menu bar to refresh the data. +The `shakes*` index pattern appears. -. Click the caret to the right of the current index pattern, and select `ba*`. +. To make `ba*` the current index, click the index pattern dropdown, then select `ba*`. + By default, all fields are shown for each matching document. -. In the search field, enter the following string: +. In the search field, enter: + [source,text] account_number<100 AND balance>47500 @@ -25,11 +24,10 @@ excess of 47,500. Results appear for account numbers 8, 32, 78, 85, and 97. [role="screenshot"] image::images/tutorial-discover-2.png[] + -. To choose which -fields to display, hover the pointer over the list of *Available fields* -and then click *add* next to each field you want include as a column in the table. +. Hover over the list of *Available fields*, then +click *add* next to each field you want include as a column in the table. + -For example, if you add the `account_number` field, the display changes to a list of five +For example, when you add the `account_number` field, the display changes to a list of five account numbers. + [role="screenshot"] From 9484012fdf0bcbaa43266246c59c44bfe1cf48d3 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 11 Mar 2020 08:58:33 -0500 Subject: [PATCH 07/13] [DOCS] Removed experimental from KQL (#59896) --- docs/management/managing-fields.asciidoc | 75 +++++++++++------------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/docs/management/managing-fields.asciidoc b/docs/management/managing-fields.asciidoc index b54f4fe5194ad..1a1bcec10ab50 100644 --- a/docs/management/managing-fields.asciidoc +++ b/docs/management/managing-fields.asciidoc @@ -1,8 +1,8 @@ [[managing-fields]] -== Index Patterns and Fields +== Index patterns and fields The *Index patterns* UI helps you create and manage -the index patterns that retrieve your data from Elasticsearch. +the index patterns that retrieve your data from {es}. [role="screenshot"] image::images/management-index-patterns.png[] @@ -10,8 +10,8 @@ image::images/management-index-patterns.png[] [float] === Create an index pattern -An index pattern is the glue that connects Kibana to your Elasticsearch data. Create an -index pattern whenever you load your own data into Kibana. To get started, +An index pattern is the glue that connects {kib} to your {es} data. Create an +index pattern whenever you load your own data into {kib}. To get started, click *Create index pattern*, and then follow the guided steps. Refer to <> for the types of index patterns that you can create. @@ -33,7 +33,7 @@ you create is automatically designated as the default pattern. The default index pattern is loaded when you open *Discover*. * *Refresh the index fields list.* You can refresh the index fields list to -pick up any newly-added fields. Doing so also resets Kibana’s popularity counters +pick up any newly-added fields. Doing so also resets the {kib} popularity counters for the fields. The popularity counters are used in *Discover* to sort fields in lists. * [[delete-pattern]]*Delete the index pattern.* This action removes the pattern from the list of @@ -60,7 +60,7 @@ Kibana has field formatters for the following field types: * <> [[field-formatters-string]] -=== String Field Formatters +=== String field formatters String fields support the `String` and `Url` formatters. @@ -69,7 +69,7 @@ include::field-formatters/string-formatter.asciidoc[] include::field-formatters/url-formatter.asciidoc[] [[field-formatters-date]] -=== Date Field Formatters +=== Date field formatters Date fields support the `Date`, `Url`, and `String` formatters. @@ -81,19 +81,19 @@ include::field-formatters/string-formatter.asciidoc[] include::field-formatters/url-formatter.asciidoc[] [[field-formatters-geopoint]] -=== Geographic Point Field Formatters +=== Geographic point field formatters Geographic point fields support the `String` formatter. include::field-formatters/string-formatter.asciidoc[] [[field-formatters-numeric]] -=== Numeric Field Formatters +=== Numeric field formatters Numeric fields support the `Url`, `Bytes`, `Duration`, `Number`, `Percentage`, `String`, and `Color` formatters. The `Bytes`, `Number`, and `Percentage` formatters enable you to choose the display formats of numbers in this field using -the <> syntax that Kibana maintains. +the <> syntax that {kib} maintains. include::field-formatters/url-formatter.asciidoc[] @@ -104,25 +104,22 @@ include::field-formatters/duration-formatter.asciidoc[] include::field-formatters/color-formatter.asciidoc[] [[scripted-fields]] -=== Scripted Fields +=== Scripted fields -Scripted fields compute data on the fly from the data in your Elasticsearch indices. Scripted field data is shown on -the Discover tab as part of the document data, and you can use scripted fields in your visualizations. -Scripted field values are computed at query time so they aren't indexed and cannot be searched using Kibana's default -query language. However they can be queried using Kibana's new <>. Scripted -fields are also supported in the filter bar. +Scripted fields compute data on the fly from the data in your {es} indices. The data is shown on +the Discover tab as part of the document data, and you can use scripted fields in your visualizations. You query scripted fields with the <>, and can filter them using the filter bar. The scripted field values are computed at query time, so they aren't indexed and cannot be searched using the {kib} default +query language. WARNING: Computing data on the fly with scripted fields can be very resource intensive and can have a direct impact on -Kibana's performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are +{kib} performance. Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. -When you define a scripted field in Kibana, you have a choice of scripting languages. Starting with 5.0, the default +When you define a scripted field in {kib}, you have a choice of scripting languages. In 5.0 and later, the default options are {ref}/modules-scripting-expression.html[Lucene expressions] and {ref}/modules-scripting-painless.html[Painless]. -While you can use other scripting languages if you enable dynamic scripting for them in Elasticsearch, this is not recommended +While you can use other scripting languages if you enable dynamic scripting for them in {es}, this is not recommended because they cannot be sufficiently {ref}/modules-scripting-security.html[sandboxed]. -WARNING: Use of Groovy, JavaScript, and Python scripting is deprecated starting in Elasticsearch 5.0, and support for those -scripting languages will be removed in the future. +WARNING: In 5.0 and later, Groovy, JavaScript, and Python scripting are deprecated and unsupported. You can reference any single value numeric field in your expressions, for example: @@ -130,44 +127,40 @@ You can reference any single value numeric field in your expressions, for exampl doc['field_name'].value ---- -For more background on scripted fields and additional examples, refer to this blog: -https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in Kibana scripted fields] +For more information on scripted fields and additional examples, refer to +https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless in {kib} scripted fields] [float] [[create-scripted-field]] -=== Creating a Scripted Field -To create a scripted field: +=== Create a scripted field -. Go to *Management > Kibana > Index Patterns* +. Go to *Management > {kib} > Index Patterns* . Select the index pattern you want to add a scripted field to. -. Go to the pattern's *Scripted fields* tab. -. Click *Add scripted field*. +. Go to the *Scripted fields* tab for the index pattern, then click *Add scripted field*. . Enter a name for the scripted field. . Enter the expression that you want to use to compute a value on the fly from your index data. . Click *Create field*. -For more information about scripted fields in Elasticsearch, see +For more information about scripted fields in {es}, see {ref}/modules-scripting.html[Scripting]. [float] [[update-scripted-field]] -=== Updating a Scripted Field -To modify a scripted field: +=== Update a scripted field -. Go to *Management > Kibana > Index Patterns* -. Click the index pattern's *Scripted fields* tab. +. Go to *Management > {kib} > Index Patterns* +. Click the *Scripted fields* tab for the index pattern. . Click the *Edit* button for the scripted field you want to change. -. Make your changes and then click *Save field* to update the field. +. Make your changes, then click *Save field*. -WARNING: Keep in mind that there's no built-in validation of a scripted field. If your scripts are buggy, you'll get +WARNING: Built-in validation is unsupported for scripted fields. If your scripts are buggy, you'll get exceptions whenever you try to view the dynamically generated data. [float] [[delete-scripted-field]] -=== Deleting a Scripted Field -To delete a scripted field: +=== Delete a scripted field -. Go to *Management > Kibana > Index Patterns* -. Click the index pattern's *Scripted fields* tab. -. Click the *Delete* button for the scripted field you want to remove. -. Click *Delete* in the confirmation window. +. Go to *Management > {kib} > Index Patterns* +. Click the *Scripted fields* tab for the index pattern. +. Click *Delete* for the scripted field you want to remove. +. Click *Delete* on the confirmation window. From d781b3e6276ae1685bebc110ee118a4954e77212 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 11 Mar 2020 06:59:32 -0700 Subject: [PATCH 08/13] [ML] Updates Jest snapshots (#59897) https://github.com/elastic/kibana/pull/59782 was merged with failing test. Signed-off-by: Tyler Smalley --- .../transform_list.test.tsx.snap | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap index e7a5e027e6f8d..e2de4c0ea1f6c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/transform_list.test.tsx.snap @@ -1,28 +1,23 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Transform: Transform List Minimal initialization 1`] = ` - - - - Create your first transform - , - ] - } - data-test-subj="transformNoTransformsFound" - title={ -

- No transforms found -

- } - /> - + + Create your first transform + , + ] + } + data-test-subj="transformNoTransformsFound" + title={ +

+ No transforms found +

+ } +/> `; From e6327d32b051db560512744dd903ddf67f45ce10 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 11 Mar 2020 16:01:07 +0200 Subject: [PATCH 09/13] [SIEM][CASE] ServiceNow executor (#58894) * Refactor structure * Init ServiceNow class * Add constants * Add configuration scheme * Refactor configuration schema * Refactor parameters schema * Create new types * Add supported source fields * Create helpers * Create ServiceNow lib * Push incident * Declare private methods * Create UpdateIncident type * Create updateIncident method * Create executor actions * Refactor response * Test helpers * Remove unnecessary validation * Fix validation errors * Throw error for unsupported actions * Create mock incident * Test executor * Test ServiceNow lib * Convert to camelCase * Remove caller_id * Refactor helpers * Refactor schema * Remove executorAction * Test action handlers * Refactor tests * Create and update comments * Remove closure option & change attribute name * Fix tests * Change lib structure * Validate empty mapping * Fix functional tests * Fix type * Change API to only add comments through incident's API * Add instruction to README * Change API version * Test * Test simulator * Fix version on tests * Remove SIEM reference in README --- x-pack/plugins/actions/README.md | 374 ++++++++++-------- .../lib/post_servicenow.ts | 28 -- .../builtin_action_types/servicenow.test.ts | 279 ------------- .../server/builtin_action_types/servicenow.ts | 171 -------- .../servicenow/action_handlers.test.ts | 157 ++++++++ .../servicenow/action_handlers.ts | 78 ++++ .../servicenow/constants.ts | 8 + .../servicenow/helpers.test.ts | 83 ++++ .../servicenow/helpers.ts | 38 ++ .../servicenow/index.test.ts | 256 ++++++++++++ .../builtin_action_types/servicenow/index.ts | 115 ++++++ .../servicenow/lib/constants.ts | 10 + .../servicenow/lib/index.test.ts | 232 +++++++++++ .../servicenow/lib/index.ts | 129 ++++++ .../servicenow/lib/types.ts | 30 ++ .../builtin_action_types/servicenow/mock.ts | 103 +++++ .../builtin_action_types/servicenow/schema.ts | 55 +++ .../servicenow/translations.ts | 53 +++ .../builtin_action_types/servicenow/types.ts | 56 +++ .../common/fixtures/plugins/actions/index.ts | 2 +- .../plugins/actions/servicenow_simulation.ts | 114 +++--- .../builtin_action_types/servicenow.ts | 204 ++++++---- 22 files changed, 1786 insertions(+), 789 deletions(-) delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts delete mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index c3ca0a16df797..d217d26e84836 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -10,8 +10,7 @@ The Kibana actions plugin provides a framework to create executable actions. You - Execute an action, passing it a parameter object. - Perform CRUD operations on actions. ------ - +--- Table of Contents @@ -61,15 +60,18 @@ Table of Contents - [`config`](#config-5) - [`secrets`](#secrets-5) - [`params`](#params-5) + - [ServiceNow](#servicenow) + - [`config`](#config-6) + - [`secrets`](#secrets-6) + - [`params`](#params-6) - [Command Line Utility](#command-line-utility) - ## Terminology -**Action Type**: A programatically defined integration with another service, with an expected set of configuration and parameters properties, typically defined with a schema. Plugins can add new +**Action Type**: A programatically defined integration with another service, with an expected set of configuration and parameters properties, typically defined with a schema. Plugins can add new action types. -**Action**: A configuration object associated with an action type, that is ready to be executed. The configuration is persisted via Saved Objects, and some/none/all of the configuration properties can be stored encrypted. +**Action**: A configuration object associated with an action type, that is ready to be executed. The configuration is persisted via Saved Objects, and some/none/all of the configuration properties can be stored encrypted. ## Usage @@ -78,36 +80,37 @@ action types. 3. Use alerts to execute actions or execute manually (see firing actions). ## Kibana Actions Configuration + Implemented under the [Actions Config](./server/actions_config.ts). ### Configuration Options Built-In-Actions are configured using the _xpack.actions_ namespoace under _kibana.yml_, and have the following configuration options: -| Namespaced Key | Description | Type | -| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | -| _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | -| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | +| Namespaced Key | Description | Type | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | +| _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | +| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | #### Whitelisting Built-in Action Types + It is worth noting that the **whitelistedHosts** configuation applies to built-in action types (such as Slack, or PagerDuty) as well. Uniquely, the _PagerDuty Action Type_ has been configured to support the service's Events API (at _https://events.pagerduty.com/v2/enqueue_, which you can read about [here](https://v2.developer.pagerduty.com/docs/events-api-v2)) as a default, but this too, must be included in the whitelist before the PagerDuty action can be used. - ### Configuration Utilities This module provides a Utilities for interacting with the configuration. -| Method | Arguments | Description | Return Type | -| --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| isWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will always return `true`. | Boolean | -| isWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will always return `true`. | Boolean | -| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | -| ensureWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will never throw. | No return value, throws if URI isn't whitelisted | -| ensureWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will never throw | No return value, throws if Hostname isn't whitelisted | -| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | +| Method | Arguments | Description | Return Type | +| ------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| isWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will always return `true`. | Boolean | +| isWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and returns `true` if it is whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will always return `true`. | Boolean | +| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | +| ensureWhitelistedUri | _uri_: The URI you wish to validate is whitelisted | Validates whether the URI is whitelisted. This checks the configuration and validates that the hostname of the URI is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all URI's are whitelisted (using an "\*") then it will never throw. | No return value, throws if URI isn't whitelisted | +| ensureWhitelistedHostname | _hostname_: The Hostname you wish to validate is whitelisted | Validates whether the Hostname is whitelisted. This checks the configuration and validates that the hostname is in the list of whitelisted Hosts and throws an error if it is not whitelisted. If the configuration says that all Hostnames are whitelisted (using an "\*") then it will never throw | No return value, throws if Hostname isn't whitelisted | +| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | ## Action types @@ -117,38 +120,37 @@ This module provides a Utilities for interacting with the configuration. The following table describes the properties of the `options` object. -|Property|Description|Type| -|---|---|---| -|id|Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types.|string| -|name|A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types.|string| -|unencryptedAttributes|A list of opt-out attributes that don't need to be encrypted. These attributes won't need to be re-entered on import / export when the feature becomes available. These attributes will also be readable / displayed when it comes to a table / edit screen.|array of strings| -|validate.params|When developing an action type, it needs to accept parameters to know what to do with the action. (Example to, from, subject, body of an email). See the current built-in email action type for an example of the state-of-the-art validation.

Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message|schema / validation function| -|validate.config|Similar to params, a config is required when creating an action (for example host, port, username, and password of an email server). |schema / validation function| -|executor|This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below.|Function| +| Property | Description | Type | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string | +| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string | +| unencryptedAttributes | A list of opt-out attributes that don't need to be encrypted. These attributes won't need to be re-entered on import / export when the feature becomes available. These attributes will also be readable / displayed when it comes to a table / edit screen. | array of strings | +| validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example to, from, subject, body of an email). See the current built-in email action type for an example of the state-of-the-art validation.

Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function | +| validate.config | Similar to params, a config is required when creating an action (for example host, port, username, and password of an email server). | schema / validation function | +| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | -**Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur. +**Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur. ### Executor -This is the primary function for an action type. Whenever the action needs to execute, this function will perform the action. It receives a variety of parameters. The following table describes the properties that the executor receives. +This is the primary function for an action type. Whenever the action needs to execute, this function will perform the action. It receives a variety of parameters. The following table describes the properties that the executor receives. **executor(options)** -|Property|Description| -|---|---| -|actionId|The action saved object id that the action type is executing for.| -|config|The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type.| -|params|Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function.| -|services.callCluster(path, opts)|Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.

**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR.| -|services.savedObjectsClient|This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled).| -|services.log(tags, [data], [timestamp])|Use this to create server logs. (This is the same function as server.log)| +| Property | Description | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| actionId | The action saved object id that the action type is executing for. | +| config | The decrypted configuration given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | +| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | +| services.callCluster(path, opts) | Use this to do Elasticsearch queries on the cluster Kibana connects to. This function is the same as any other `callCluster` in Kibana.

**NOTE**: This currently authenticates as the Kibana internal user, but will change in a future PR. | +| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | +| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | ### Example -The built-in email action type provides a good example of creating an action type with non-trivial configuration and params: +The built-in email action type provides a good example of creating an action type with non-trivial configuration and params: [x-pack/plugins/actions/server/builtin_action_types/email.ts](server/builtin_action_types/email.ts) - ## RESTful API Using an action type requires an action to be created that will contain and encrypt configuration for a given action type. See below for CRUD operations using the API. @@ -157,20 +159,20 @@ Using an action type requires an action to be created that will contain and encr Payload: -|Property|Description|Type| -|---|---|---| -|name|A name to reference and search in the future. This value will be used to populate dropdowns.|string| -|actionTypeId|The id value of the action type you want to call when the action executes.|string| -|config|The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined.|object| -|secrets|The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined.|object| +| Property | Description | Type | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| name | A name to reference and search in the future. This value will be used to populate dropdowns. | string | +| actionTypeId | The id value of the action type you want to call when the action executes. | string | +| config | The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined. | object | +| secrets | The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined. | object | ### `DELETE /api/action/{id}`: Delete action Params: -|Property|Description|Type| -|---|---|---| -|id|The id of the action you're trying to delete.|string| +| Property | Description | Type | +| -------- | --------------------------------------------- | ------ | +| id | The id of the action you're trying to delete. | string | ### `GET /api/action/_find`: Find actions @@ -182,9 +184,9 @@ See the [saved objects API documentation for find](https://www.elastic.co/guide/ Params: -|Property|Description|Type| -|---|---|---| -|id|The id of the action you're trying to get.|string| +| Property | Description | Type | +| -------- | ------------------------------------------ | ------ | +| id | The id of the action you're trying to get. | string | ### `GET /api/action/types`: List action types @@ -194,31 +196,31 @@ No parameters. Params: -|Property|Description|Type| -|---|---|---| -|id|The id of the action you're trying to update.|string| +| Property | Description | Type | +| -------- | --------------------------------------------- | ------ | +| id | The id of the action you're trying to update. | string | Payload: -|Property|Description|Type| -|---|---|---| -|name|A name to reference and search in the future. This value will be used to populate dropdowns.|string| -|config|The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined.|object| -|secrets|The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined.|object| +| Property | Description | Type | +| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| name | A name to reference and search in the future. This value will be used to populate dropdowns. | string | +| config | The configuration the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if config validation is defined. | object | +| secrets | The secrets the action type expects. See related action type to see what attributes are expected. This will also validate against the action type if secrets validation is defined. | object | ### `POST /api/action/{id}/_execute`: Execute action Params: -|Property|Description|Type| -|---|---|---| -|id|The id of the action you're trying to execute.|string| +| Property | Description | Type | +| -------- | ---------------------------------------------- | ------ | +| id | The id of the action you're trying to execute. | string | Payload: -|Property|Description|Type| -|---|---|---| -|params|The parameters the action type requires for the execution.|object| +| Property | Description | Type | +| -------- | ---------------------------------------------------------- | ------ | +| params | The parameters the action type requires for the execution. | object | ## Firing actions @@ -228,12 +230,12 @@ The plugin exposes an execute function that you can use to run actions. The following table describes the properties of the `options` object. -|Property|Description|Type| -|---|---|---| -|id|The id of the action you want to execute.|string| -|params|The `params` value to give the action type executor.|object| -|spaceId|The space id the action is within.|string| -|apiKey|The Elasticsearch API key to use for context. (Note: only required and used when security is enabled).|string| +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------ | ------ | +| id | The id of the action you want to execute. | string | +| params | The `params` value to give the action type executor. | object | +| spaceId | The space id the action is within. | string | +| apiKey | The Elasticsearch API key to use for context. (Note: only required and used when security is enabled). | string | ## Example @@ -256,23 +258,25 @@ server.plugins.actions.execute({ Kibana ships with a set of built-in action types: -|Type|Id|Description| -|---|---|---| -|[Server log](#server-log)|`.log`|Logs messages to the Kibana log using `server.log()`| -|[Email](#email)|`.email`|Sends an email using SMTP| -|[Slack](#slack)|`.slack`|Posts a message to a slack channel| -|[Index](#index)|`.index`|Indexes document(s) into Elasticsearch| -|[Webhook](#webhook)|`.webhook`|Send a payload to a web service using HTTP POST or PUT| -|[PagerDuty](#pagerduty)|`.pagerduty`|Trigger, resolve, or acknowlege an incident to a PagerDuty service| +| Type | Id | Description | +| ------------------------- | ------------- | ------------------------------------------------------------------ | +| [Server log](#server-log) | `.log` | Logs messages to the Kibana log using `server.log()` | +| [Email](#email) | `.email` | Sends an email using SMTP | +| [Slack](#slack) | `.slack` | Posts a message to a slack channel | +| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | +| [Webhook](#webhook) | `.webhook` | Send a payload to a web service using HTTP POST or PUT | +| [PagerDuty](#pagerduty) | `.pagerduty` | Trigger, resolve, or acknowlege an incident to a PagerDuty service | +| [ServiceNow](#servicenow) | `.servicenow` | Create or update an incident to a ServiceNow instance | + +--- ----- ## Server log ID: `.log` The params properties are modelled after the arguments to the [Hapi.server.log()](https://hapijs.com/api#-serverlogtags-data-timestamp) function. -### `config` +### `config` This action has no `config` properties. @@ -282,12 +286,13 @@ This action type has no `secrets` properties. ### `params` -|Property|Description|Type| -|---|---|---| -|message|The message to log.|string| -|tags|Tags associated with the message to log.|string[] _(optional)_| +| Property | Description | Type | +| -------- | ---------------------------------------- | --------------------- | +| message | The message to log. | string | +| tags | Tags associated with the message to log. | string[] _(optional)_ | + +--- ----- ## Email ID: `.email` @@ -296,50 +301,50 @@ This action type uses [nodemailer](https://nodemailer.com/about/) to send emails ### `config` -Either the property `service` must be provided, or the `host` and `port` properties must be provided. If `service` is provided, `host`, `port` and `secure` are ignored. For more information on the `gmail` service value specifically, see the [nodemailer gmail documentation](https://nodemailer.com/usage/using-gmail/). +Either the property `service` must be provided, or the `host` and `port` properties must be provided. If `service` is provided, `host`, `port` and `secure` are ignored. For more information on the `gmail` service value specifically, see the [nodemailer gmail documentation](https://nodemailer.com/usage/using-gmail/). -The `secure` property defaults to `false`. See the [nodemailer TLS documentation](https://nodemailer.com/smtp/#tls-options) for more information. +The `secure` property defaults to `false`. See the [nodemailer TLS documentation](https://nodemailer.com/smtp/#tls-options) for more information. -The `from` field can be specified as in typical `"user@host-name"` format, or as `"human name "` format. See the [nodemailer address documentation](https://nodemailer.com/message/addresses/) for more information. +The `from` field can be specified as in typical `"user@host-name"` format, or as `"human name "` format. See the [nodemailer address documentation](https://nodemailer.com/message/addresses/) for more information. -|Property|Description|Type| -|---|---|---| -|service|the name of a [well-known email service provider](https://nodemailer.com/smtp/well-known/)|string _(optional)_| -|host|host name of the service provider|string _(optional)_| -|port|port number of the service provider|number _(optional)_| -|secure|whether to use TLS with the service provider|boolean _(optional)_| -|from|the from address for all emails sent with this action type|string| +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------ | -------------------- | +| service | the name of a [well-known email service provider](https://nodemailer.com/smtp/well-known/) | string _(optional)_ | +| host | host name of the service provider | string _(optional)_ | +| port | port number of the service provider | number _(optional)_ | +| secure | whether to use TLS with the service provider | boolean _(optional)_ | +| from | the from address for all emails sent with this action type | string | ### `secrets` -|Property|Description|Type| -|---|---|---| -|user|userid to use with the service provider|string| -|password|password to use with the service provider|string| +| Property | Description | Type | +| -------- | ----------------------------------------- | ------ | +| user | userid to use with the service provider | string | +| password | password to use with the service provider | string | ### `params` There must be at least one entry in the `to`, `cc` and `bcc` arrays. -The message text will be sent as both plain text and html text. Additional function may be provided later. +The message text will be sent as both plain text and html text. Additional function may be provided later. The `to`, `cc`, and `bcc` array entries can be in the same format as the `from` property described in the config object above. -|Property|Description|Type| -|---|---|---| -|to|list of to addressees|string[] _(optional)_| -|cc|list of cc addressees|string[] _(optional)_| -|bcc|list of bcc addressees|string[] _(optional)_| -|subject|the subject line of the email|string| -|message|the message text|string| +| Property | Description | Type | +| -------- | ----------------------------- | --------------------- | +| to | list of to addressees | string[] _(optional)_ | +| cc | list of cc addressees | string[] _(optional)_ | +| bcc | list of bcc addressees | string[] _(optional)_ | +| subject | the subject line of the email | string | +| message | the message text | string | ----- +--- ## Slack ID: `.slack` -This action type interfaces with the [Slack Incoming Webhooks feature](https://api.slack.com/incoming-webhooks). Currently the params property `message` will be used as the `text` property of the Slack incoming message. Additional function may be provided later. +This action type interfaces with the [Slack Incoming Webhooks feature](https://api.slack.com/incoming-webhooks). Currently the params property `message` will be used as the `text` property of the Slack incoming message. Additional function may be provided later. ### `config` @@ -347,29 +352,29 @@ This action type has no `config` properties. ### `secrets` -|Property|Description|Type| -|---|---|---| -|webhookUrl|the url of the Slack incoming webhook|string| +| Property | Description | Type | +| ---------- | ------------------------------------- | ------ | +| webhookUrl | the url of the Slack incoming webhook | string | ### `params` -|Property|Description|Type| -|---|---|---| -|message|the message text|string| +| Property | Description | Type | +| -------- | ---------------- | ------ | +| message | the message text | string | ----- +--- ## Index ID: `.index` -The config and params properties are modelled after the [Watcher Index Action](https://www.elastic.co/guide/en/elasticsearch/reference/master/actions-index.html). The index can be set in the config or params, and if set in config, then the index set in the params will be ignored. +The config and params properties are modelled after the [Watcher Index Action](https://www.elastic.co/guide/en/elasticsearch/reference/master/actions-index.html). The index can be set in the config or params, and if set in config, then the index set in the params will be ignored. ### `config` -|Property|Description|Type| -|---|---|---| -|index|The Elasticsearch index to index into.|string _(optional)_| +| Property | Description | Type | +| -------- | -------------------------------------- | ------------------- | +| index | The Elasticsearch index to index into. | string _(optional)_ | ### `secrets` @@ -377,81 +382,114 @@ This action type has no `secrets` properties. ### `params` -|Property|Description|Type| -|---|---|---| -|index|The Elasticsearch index to index into.|string _(optional)_| -|doc_id|The optional _id of the document.|string _(optional)_| -|execution_time_field|The field that will store/index the action execution time.|string _(optional)_| -|refresh|Setting of the refresh policy for the write request|boolean _(optional)_| -|body|The documument body/bodies to index.|object or object[]| +| Property | Description | Type | +| -------------------- | ---------------------------------------------------------- | -------------------- | +| index | The Elasticsearch index to index into. | string _(optional)_ | +| doc_id | The optional \_id of the document. | string _(optional)_ | +| execution_time_field | The field that will store/index the action execution time. | string _(optional)_ | +| refresh | Setting of the refresh policy for the write request | boolean _(optional)_ | +| body | The documument body/bodies to index. | object or object[] | + +--- ----- ## Webhook ID: `.webhook` The webhook action uses [axios](https://github.com/axios/axios) to send a POST or PUT request to a web service. -### `config` +### `config` -|Property|Description|Type| -|---|---|---| -|url|Request URL|string| -|method|HTTP request method, either `post`_(default)_ or `put`|string _(optional)_| -|headers|Key-value pairs of the headers to send with the request|object, keys and values are strings _(optional)_| +| Property | Description | Type | +| -------- | ------------------------------------------------------- | ------------------------------------------------ | +| url | Request URL | string | +| method | HTTP request method, either `post`_(default)_ or `put` | string _(optional)_ | +| headers | Key-value pairs of the headers to send with the request | object, keys and values are strings _(optional)_ | -### `secrets` +### `secrets` -|Property|Description|Type| -|---|---|---| -|user|Username for HTTP Basic authentication|string _(optional)_| -|password|Password for HTTP Basic authentication|string _(optional)_| +| Property | Description | Type | +| -------- | -------------------------------------- | ------------------- | +| user | Username for HTTP Basic authentication | string _(optional)_ | +| password | Password for HTTP Basic authentication | string _(optional)_ | -### `params` +### `params` -|Property|Description|Type| -|---|---|---| -|body|The HTTP request body|string _(optional)_| +| Property | Description | Type | +| -------- | --------------------- | ------------------- | +| body | The HTTP request body | string _(optional)_ | ----- +--- ## PagerDuty -ID: `.pagerduty` +ID: `.pagerduty` -The PagerDuty action uses the [V2 Events API](https://v2.developer.pagerduty.com/docs/events-api-v2) to trigger, acknowlege, and resolve PagerDuty alerts. +The PagerDuty action uses the [V2 Events API](https://v2.developer.pagerduty.com/docs/events-api-v2) to trigger, acknowlege, and resolve PagerDuty alerts. -### `config` +### `config` -|Property|Description|Type| -|---|---|---| -|apiUrl|PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`|string _(optional)_| +| Property | Description | Type | +| -------- | -------------------------------------------------------------------------- | ------------------- | +| apiUrl | PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue` | string _(optional)_ | ### `secrets` -|Property|Description|Type| -|---|---|---| -|routingKey|This is the 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset.|string| +| Property | Description | Type | +| ---------- | ---------------------------------------------------------------------------------------------------------- | ------ | +| routingKey | This is the 32 character PagerDuty Integration Key for an integration on a service or on a global ruleset. | string | -### `params` +### `params` -|Property|Description|Type| -|---|---|---| -|eventAction|One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details.| string _(optional)_| -|dedupKey|All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. Defaults to `action:`. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_| -|summary|A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_| -|source|The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `.| string _(optional)_| -|severity|The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_.| string _(optional)_| -|timestamp|An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated.| string _(optional)_| -|component|The component of the source machine that is responsible for the event, for example `mysql` or `eth0`.| string _(optional)_| -|group|Logical grouping of components of a service, for example `app-stack`.| string _(optional)_| -|class|The class/type of the event, for example `ping failure` or `cpu load`.| string _(optional)_| +| Property | Description | Type | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| eventAction | One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details. | string _(optional)_ | +| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. Defaults to `action:`. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ | +| summary | A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_ | +| source | The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `. | string _(optional)_ | +| severity | The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_. | string _(optional)_ | +| timestamp | An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated. | string _(optional)_ | +| component | The component of the source machine that is responsible for the event, for example `mysql` or `eth0`. | string _(optional)_ | +| group | Logical grouping of components of a service, for example `app-stack`. | string _(optional)_ | +| class | The class/type of the event, for example `ping failure` or `cpu load`. | string _(optional)_ | For more details see [PagerDuty v2 event parameters](https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2). +--- + +## ServiceNow + +ID: `.servicenow` + +The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/app.do#!/rest_api_doc?v=orlando&id=c_TableAPI) to create and update ServiceNow incidents. + +### `config` + +| Property | Description | Type | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| apiUrl | ServiceNow instance URL. | string | +| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object | + +### `secrets` + +| Property | Description | Type | +| -------- | -------------------------------------- | ------ | +| username | Username for HTTP Basic authentication | string | +| password | Password for HTTP Basic authentication | string | + +### `params` + +| Property | Description | Type | +| ----------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| caseId | The case id | string | +| title | The title of the case | string _(optional)_ | +| description | The description of the case | string _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| incidentID | The id of the incident in ServiceNow . If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | + # Command Line Utility -The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: +The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: ```console $ kbn-action create .slack "post to slack" '{"webhookUrl": "https://hooks.slack.com/services/T0000/B0000/XXXX"}' @@ -467,4 +505,4 @@ $ kbn-action create .slack "post to slack" '{"webhookUrl": "https://hooks.slack. "updated_at": "2019-06-26T17:55:42.728Z", "version": "WzMsMV0=" } -``` \ No newline at end of file +``` diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts deleted file mode 100644 index cfd3a9d70dc93..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_servicenow.ts +++ /dev/null @@ -1,28 +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 axios, { AxiosResponse } from 'axios'; -import { Services } from '../../types'; -import { ParamsType, SecretsType } from '../servicenow'; - -interface PostServiceNowOptions { - apiUrl: string; - data: ParamsType; - headers: Record; - services?: Services; - secrets: SecretsType; -} - -// post an event to serviceNow -export async function postServiceNow(options: PostServiceNowOptions): Promise { - const { apiUrl, data, headers, secrets } = options; - const axiosOptions = { - headers, - validateStatus: () => true, - auth: secrets, - }; - return axios.post(`${apiUrl}/api/now/v1/table/incident`, data, axiosOptions); -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts deleted file mode 100644 index 9ae96cb23a5c3..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.test.ts +++ /dev/null @@ -1,279 +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. - */ - -jest.mock('./lib/post_servicenow', () => ({ - postServiceNow: jest.fn(), -})); - -import { getActionType } from './servicenow'; -import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; -import { validateConfig, validateSecrets, validateParams } from '../lib'; -import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; -import { postServiceNow } from './lib/post_servicenow'; -import { createActionTypeRegistry } from './index.test'; -import { configUtilsMock } from '../actions_config.mock'; - -const postServiceNowMock = postServiceNow as jest.Mock; - -const ACTION_TYPE_ID = '.servicenow'; - -const services: Services = { - callCluster: async (path: string, opts: any) => {}, - savedObjectsClient: savedObjectsClientMock.create(), -}; - -let actionType: ActionType; - -const mockServiceNow = { - config: { - apiUrl: 'www.servicenowisinkibanaactions.com', - }, - secrets: { - password: 'secret-password', - username: 'secret-username', - }, - params: { - comments: 'hello cool service now incident', - short_description: 'this is a cool service now incident', - }, -}; - -beforeAll(() => { - const { actionTypeRegistry } = createActionTypeRegistry(); - actionType = actionTypeRegistry.get(ACTION_TYPE_ID); -}); - -describe('get()', () => { - test('should return correct action type', () => { - expect(actionType.id).toEqual(ACTION_TYPE_ID); - expect(actionType.name).toEqual('ServiceNow'); - }); -}); - -describe('validateConfig()', () => { - test('should validate and pass when config is valid', () => { - const { config } = mockServiceNow; - expect(validateConfig(actionType, config)).toEqual(config); - }); - - test('should validate and throw error when config is invalid', () => { - expect(() => { - validateConfig(actionType, { shouldNotBeHere: true }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]"` - ); - }); - - test('should validate and pass when the servicenow url is whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...configUtilsMock, - ensureWhitelistedUri: url => { - expect(url).toEqual('https://events.servicenow.com/v2/enqueue'); - }, - }, - }); - - expect( - validateConfig(actionType, { apiUrl: 'https://events.servicenow.com/v2/enqueue' }) - ).toEqual({ apiUrl: 'https://events.servicenow.com/v2/enqueue' }); - }); - - test('config validation returns an error if the specified URL isnt whitelisted', () => { - actionType = getActionType({ - configurationUtilities: { - ...configUtilsMock, - ensureWhitelistedUri: _ => { - throw new Error(`target url is not whitelisted`); - }, - }, - }); - - expect(() => { - validateConfig(actionType, { apiUrl: 'https://events.servicenow.com/v2/enqueue' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type config: error configuring servicenow action: target url is not whitelisted"` - ); - }); -}); - -describe('validateSecrets()', () => { - test('should validate and pass when secrets is valid', () => { - const { secrets } = mockServiceNow; - expect(validateSecrets(actionType, secrets)).toEqual(secrets); - }); - - test('should validate and throw error when secrets is invalid', () => { - expect(() => { - validateSecrets(actionType, { username: false }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [password]: expected value of type [string] but got [undefined]"` - ); - - expect(() => { - validateSecrets(actionType, { username: false, password: 'hello' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action type secrets: [username]: expected value of type [string] but got [boolean]"` - ); - }); -}); - -describe('validateParams()', () => { - test('should validate and pass when params is valid', () => { - const { params } = mockServiceNow; - expect(validateParams(actionType, params)).toEqual(params); - }); - - test('should validate and throw error when params is invalid', () => { - expect(() => { - validateParams(actionType, { eventAction: 'ackynollage' }); - }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [short_description]: expected value of type [string] but got [undefined]"` - ); - }); -}); - -describe('execute()', () => { - beforeEach(() => { - postServiceNowMock.mockReset(); - }); - const { config, params, secrets } = mockServiceNow; - test('should succeed with valid params', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 201, data: 'data-here' }; - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - const { apiUrl, data, headers } = postServiceNowMock.mock.calls[0][0]; - expect({ apiUrl, data, headers, secrets }).toMatchInlineSnapshot(` - Object { - "apiUrl": "www.servicenowisinkibanaactions.com", - "data": Object { - "comments": "hello cool service now incident", - "short_description": "this is a cool service now incident", - }, - "headers": Object { - "Accept": "application/json", - "Content-Type": "application/json", - }, - "secrets": Object { - "password": "secret-password", - "username": "secret-username", - }, - } - `); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "data": "data-here", - "status": "ok", - } - `); - }); - - test('should fail when postServiceNow throws', async () => { - postServiceNowMock.mockImplementation(() => { - throw new Error('doing some testing'); - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event", - "serviceMessage": "doing some testing", - "status": "error", - } - `); - }); - - test('should fail when postServiceNow returns 429', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 429, data: 'data-here' }; - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event: http status 429, retry later", - "retry": true, - "status": "error", - } - `); - }); - - test('should fail when postServiceNow returns 501', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 501, data: 'data-here' }; - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event: http status 501, retry later", - "retry": true, - "status": "error", - } - `); - }); - - test('should fail when postServiceNow returns 418', async () => { - postServiceNowMock.mockImplementation(() => { - return { status: 418, data: 'data-here' }; - }); - - const actionId = 'some-action-id'; - const executorOptions: ActionTypeExecutorOptions = { - actionId, - config, - params, - secrets, - services, - }; - const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toMatchInlineSnapshot(` - Object { - "actionId": "some-action-id", - "message": "error posting servicenow event: unexpected status 418", - "status": "error", - } - `); - }); -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts deleted file mode 100644 index 0ad435281eba4..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow.ts +++ /dev/null @@ -1,171 +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 { curry } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { schema, TypeOf } from '@kbn/config-schema'; -import { - ActionType, - ActionTypeExecutorOptions, - ActionTypeExecutorResult, - ExecutorType, -} from '../types'; -import { ActionsConfigurationUtilities } from '../actions_config'; -import { postServiceNow } from './lib/post_servicenow'; - -// config definition -export type ConfigType = TypeOf; - -const ConfigSchemaProps = { - apiUrl: schema.string(), -}; - -const ConfigSchema = schema.object(ConfigSchemaProps); - -function validateConfig( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ConfigType -) { - if (configObject.apiUrl == null) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiNullError', { - defaultMessage: 'ServiceNow [apiUrl] is required', - }); - } - try { - configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); - } catch (whitelistError) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { - defaultMessage: 'error configuring servicenow action: {message}', - values: { - message: whitelistError.message, - }, - }); - } -} -// secrets definition -export type SecretsType = TypeOf; -const SecretsSchemaProps = { - password: schema.string(), - username: schema.string(), -}; - -const SecretsSchema = schema.object(SecretsSchemaProps); - -function validateSecrets( - configurationUtilities: ActionsConfigurationUtilities, - secrets: SecretsType -) { - if (secrets.username == null) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiUserError', { - defaultMessage: 'error configuring servicenow action: no secrets [username] provided', - }); - } - if (secrets.password == null) { - return i18n.translate('xpack.actions.builtin.servicenow.servicenowApiPasswordError', { - defaultMessage: 'error configuring servicenow action: no secrets [password] provided', - }); - } -} - -// params definition - -export type ParamsType = TypeOf; - -const ParamsSchema = schema.object({ - comments: schema.maybe(schema.string()), - short_description: schema.string(), -}); - -// action type definition -export function getActionType({ - configurationUtilities, - executor = serviceNowExecutor, -}: { - configurationUtilities: ActionsConfigurationUtilities; - executor?: ExecutorType; -}): ActionType { - return { - id: '.servicenow', - name: i18n.translate('xpack.actions.builtin.servicenowTitle', { - defaultMessage: 'ServiceNow', - }), - validate: { - config: schema.object(ConfigSchemaProps, { - validate: curry(validateConfig)(configurationUtilities), - }), - secrets: schema.object(SecretsSchemaProps, { - validate: curry(validateSecrets)(configurationUtilities), - }), - params: ParamsSchema, - }, - executor, - }; -} - -// action executor - -async function serviceNowExecutor( - execOptions: ActionTypeExecutorOptions -): Promise { - const actionId = execOptions.actionId; - const config = execOptions.config as ConfigType; - const secrets = execOptions.secrets as SecretsType; - const params = execOptions.params as ParamsType; - const headers = { - Accept: 'application/json', - 'Content-Type': 'application/json', - }; - let response; - try { - response = await postServiceNow({ apiUrl: config.apiUrl, data: params, headers, secrets }); - } catch (err) { - const message = i18n.translate('xpack.actions.builtin.servicenow.postingErrorMessage', { - defaultMessage: 'error posting servicenow event', - }); - return { - status: 'error', - actionId, - message, - serviceMessage: err.message, - }; - } - if (response.status === 200 || response.status === 201 || response.status === 204) { - return { - status: 'ok', - actionId, - data: response.data, - }; - } - - if (response.status === 429 || response.status >= 500) { - const message = i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', { - defaultMessage: 'error posting servicenow event: http status {status}, retry later', - values: { - status: response.status, - }, - }); - - return { - status: 'error', - actionId, - message, - retry: true, - }; - } - - const message = i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', { - defaultMessage: 'error posting servicenow event: unexpected status {status}', - values: { - status: response.status, - }, - }); - - return { - status: 'error', - actionId, - message, - }; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts new file mode 100644 index 0000000000000..381b44439033c --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -0,0 +1,157 @@ +/* + * 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 { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { ServiceNow } from './lib'; +import { finalMapping } from './mock'; +import { Incident } from './lib/types'; + +jest.mock('./lib'); + +const ServiceNowMock = ServiceNow as jest.Mock; + +const incident: Incident = { + short_description: 'A title', + description: 'A description', +}; + +const comments = [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: undefined, + }, +]; + +describe('handleCreateIncident', () => { + beforeAll(() => { + ServiceNowMock.mockImplementation(() => { + return { + serviceNow: { + getUserID: jest.fn().mockResolvedValue('1234'), + createIncident: jest.fn().mockResolvedValue({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }), + updateIncident: jest.fn().mockResolvedValue({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }), + batchCreateComments: jest + .fn() + .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), + batchUpdateComments: jest + .fn() + .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), + }, + }; + }); + }); + + test('create an incident without comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleCreateIncident({ + serviceNow, + params: incident, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.createIncident).toHaveBeenCalled(); + expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('create an incident with comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleCreateIncident({ + serviceNow, + params: incident, + comments, + mapping: finalMapping, + }); + + expect(serviceNow.createIncident).toHaveBeenCalled(); + expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments'); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + comments: [ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); + + test('update an incident without comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params: incident, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('update an incident and create new comments', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params: incident, + comments, + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments'); + + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + comments: [ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts new file mode 100644 index 0000000000000..47120c5da096d --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -0,0 +1,78 @@ +/* + * 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 { zipWith } from 'lodash'; +import { Incident, CommentResponse } from './lib/types'; +import { + ActionHandlerArguments, + UpdateParamsType, + UpdateActionHandlerArguments, + IncidentCreationResponse, + CommentType, + CommentsZipped, +} from './types'; +import { ServiceNow } from './lib'; + +const createComments = async ( + serviceNow: ServiceNow, + incidentId: string, + key: string, + comments: CommentType[] +): Promise => { + const createdComments = await serviceNow.batchCreateComments(incidentId, comments, key); + + return zipWith(comments, createdComments, (a: CommentType, b: CommentResponse) => ({ + commentId: a.commentId, + pushedDate: b.pushedDate, + })); +}; + +export const handleCreateIncident = async ({ + serviceNow, + params, + comments, + mapping, +}: ActionHandlerArguments): Promise => { + const paramsAsIncident = params as Incident; + + const { incidentId, number, pushedDate } = await serviceNow.createIncident({ + ...paramsAsIncident, + }); + + const res: IncidentCreationResponse = { incidentId, number, pushedDate }; + + if (comments && Array.isArray(comments) && comments.length > 0) { + res.comments = [ + ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), + ]; + } + + return { ...res }; +}; + +export const handleUpdateIncident = async ({ + incidentId, + serviceNow, + params, + comments, + mapping, +}: UpdateActionHandlerArguments): Promise => { + const paramsAsIncident = params as UpdateParamsType; + + const { number, pushedDate } = await serviceNow.updateIncident(incidentId, { + ...paramsAsIncident, + }); + + const res: IncidentCreationResponse = { incidentId, number, pushedDate }; + + if (comments && Array.isArray(comments) && comments.length > 0) { + res.comments = [ + ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), + ]; + } + + return { ...res }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts new file mode 100644 index 0000000000000..a0ffd859e14ca --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/constants.ts @@ -0,0 +1,8 @@ +/* + * 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 ACTION_TYPE_ID = '.servicenow'; +export const SUPPORTED_SOURCE_FIELDS = ['title', 'comments', 'description']; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts new file mode 100644 index 0000000000000..96962b41b3c68 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { normalizeMapping, buildMap, mapParams } from './helpers'; +import { mapping, finalMapping } from './mock'; +import { SUPPORTED_SOURCE_FIELDS } from './constants'; +import { MapsType } from './types'; + +const maliciousMapping: MapsType[] = [ + { source: '__proto__', target: 'short_description', actionType: 'nothing' }, + { source: 'description', target: '__proto__', actionType: 'nothing' }, + { source: 'comments', target: 'comments', actionType: 'nothing' }, + { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, +]; + +describe('sanitizeMapping', () => { + test('remove malicious fields', () => { + const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(sanitizedMapping.every(m => m.source !== '__proto__' && m.target !== '__proto__')).toBe( + true + ); + }); + + test('remove unsuppported source fields', () => { + const normalizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); + expect(normalizedMapping).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + source: 'unsupportedSource', + target: 'comments', + actionType: 'nothing', + }), + ]) + ); + }); +}); + +describe('buildMap', () => { + test('builds sanitized Map', () => { + const finalMap = buildMap(maliciousMapping); + expect(finalMap.get('__proto__')).not.toBeDefined(); + }); + + test('builds Map correct', () => { + const final = buildMap(mapping); + expect(final).toEqual(finalMapping); + }); +}); + +describe('mapParams', () => { + test('maps params correctly', () => { + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + + const fields = mapParams(params, finalMapping); + + expect(fields).toEqual({ + short_description: 'Incident title', + description: 'Incident description', + }); + }); + + test('do not add fields not in mapping', () => { + const params = { + caseId: '123', + incidentId: '456', + title: 'Incident title', + description: 'Incident description', + }; + const fields = mapParams(params, finalMapping); + + const { title, description, ...unexpectedFields } = params; + + expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts new file mode 100644 index 0000000000000..99e67c1c43f35 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.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. + */ + +import { SUPPORTED_SOURCE_FIELDS } from './constants'; +import { MapsType, FinalMapping } from './types'; + +export const normalizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => { + // Prevent prototype pollution and remove unsupported fields + return mapping.filter( + m => m.source !== '__proto__' && m.target !== '__proto__' && fields.includes(m.source) + ); +}; + +export const buildMap = (mapping: MapsType[]): FinalMapping => { + return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { + const { source, target, actionType } = field; + fieldsMap.set(source, { target, actionType }); + fieldsMap.set(target, { target: source, actionType }); + return fieldsMap; + }, new Map()); +}; + +interface KeyAny { + [key: string]: unknown; +} + +export const mapParams = (params: any, mapping: FinalMapping) => { + return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { + const field = mapping.get(curr); + if (field) { + prev[field.target] = params[curr]; + } + return prev; + }, {}); +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts new file mode 100644 index 0000000000000..a1df243b0ee7c --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -0,0 +1,256 @@ +/* + * 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 { getActionType } from '.'; +import { ActionType, Services, ActionTypeExecutorOptions } from '../../types'; +import { validateConfig, validateSecrets, validateParams } from '../../lib'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { createActionTypeRegistry } from '../index.test'; +import { configUtilsMock } from '../../actions_config.mock'; + +import { ACTION_TYPE_ID } from './constants'; +import * as i18n from './translations'; + +import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { incidentResponse } from './mock'; + +jest.mock('./action_handlers'); + +const handleCreateIncidentMock = handleCreateIncident as jest.Mock; +const handleUpdateIncidentMock = handleUpdateIncident as jest.Mock; + +const services: Services = { + callCluster: async (path: string, opts: any) => {}, + savedObjectsClient: savedObjectsClientMock.create(), +}; + +let actionType: ActionType; + +const mockOptions = { + name: 'servicenow-connector', + actionTypeId: '.servicenow', + secrets: { + username: 'secret-username', + password: 'secret-password', + }, + config: { + apiUrl: 'https://service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'work_notes', + actionType: 'append', + }, + ], + }, + }, + params: { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', + }, + ], + }, +}; + +beforeAll(() => { + const { actionTypeRegistry } = createActionTypeRegistry(); + actionType = actionTypeRegistry.get(ACTION_TYPE_ID); +}); + +describe('get()', () => { + test('should return correct action type', () => { + expect(actionType.id).toEqual(ACTION_TYPE_ID); + expect(actionType.name).toEqual(i18n.NAME); + }); +}); + +describe('validateConfig()', () => { + test('should validate and pass when config is valid', () => { + const { config } = mockOptions; + expect(validateConfig(actionType, config)).toEqual(config); + }); + + test('should validate and throw error when config is invalid', () => { + expect(() => { + validateConfig(actionType, { shouldNotBeHere: true }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]"` + ); + }); + + test('should validate and pass when the servicenow url is whitelisted', () => { + actionType = getActionType({ + configurationUtilities: { + ...configUtilsMock, + ensureWhitelistedUri: url => { + expect(url).toEqual(mockOptions.config.apiUrl); + }, + }, + }); + + expect(validateConfig(actionType, mockOptions.config)).toEqual(mockOptions.config); + }); + + test('config validation returns an error if the specified URL isnt whitelisted', () => { + actionType = getActionType({ + configurationUtilities: { + ...configUtilsMock, + ensureWhitelistedUri: _ => { + throw new Error(`target url is not whitelisted`); + }, + }, + }); + + expect(() => { + validateConfig(actionType, mockOptions.config); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type config: error configuring servicenow action: target url is not whitelisted"` + ); + }); +}); + +describe('validateSecrets()', () => { + test('should validate and pass when secrets is valid', () => { + const { secrets } = mockOptions; + expect(validateSecrets(actionType, secrets)).toEqual(secrets); + }); + + test('should validate and throw error when secrets is invalid', () => { + expect(() => { + validateSecrets(actionType, { username: false }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: [password]: expected value of type [string] but got [undefined]"` + ); + + expect(() => { + validateSecrets(actionType, { username: false, password: 'hello' }); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action type secrets: [username]: expected value of type [string] but got [boolean]"` + ); + }); +}); + +describe('validateParams()', () => { + test('should validate and pass when params is valid', () => { + const { params } = mockOptions; + expect(validateParams(actionType, params)).toEqual(params); + }); + + test('should validate and throw error when params is invalid', () => { + expect(() => { + validateParams(actionType, {}); + }).toThrowErrorMatchingInlineSnapshot( + `"error validating action params: [caseId]: expected value of type [string] but got [undefined]"` + ); + }); +}); + +describe('execute()', () => { + beforeEach(() => { + handleCreateIncidentMock.mockReset(); + handleUpdateIncidentMock.mockReset(); + }); + + test('should create an incident', async () => { + const actionId = 'some-id'; + const { incidentId, ...rest } = mockOptions.params; + + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config: mockOptions.config, + params: { ...rest }, + secrets: mockOptions.secrets, + services, + }; + + handleCreateIncidentMock.mockImplementation(() => incidentResponse); + + const actionResponse = await actionType.executor(executorOptions); + expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); + }); + + test('should throw an error when failed to create incident', async () => { + expect.assertions(1); + const { incidentId, ...rest } = mockOptions.params; + + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config: mockOptions.config, + params: { ...rest }, + secrets: mockOptions.secrets, + services, + }; + const errorMessage = 'Failed to create incident'; + + handleCreateIncidentMock.mockImplementation(() => { + throw new Error(errorMessage); + }); + + try { + await actionType.executor(executorOptions); + } catch (error) { + expect(error.message).toEqual(errorMessage); + } + }); + + test('should update an incident', async () => { + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config: mockOptions.config, + params: { ...mockOptions.params, executorAction: 'updateIncident' }, + secrets: mockOptions.secrets, + services, + }; + + const actionResponse = await actionType.executor(executorOptions); + expect(actionResponse).toEqual({ actionId, status: 'ok' }); + }); + + test('should throw an error when failed to update an incident', async () => { + expect.assertions(1); + + const actionId = 'some-id'; + const executorOptions: ActionTypeExecutorOptions = { + actionId, + config: mockOptions.config, + params: { ...mockOptions.params, executorAction: 'updateIncident' }, + secrets: mockOptions.secrets, + services, + }; + const errorMessage = 'Failed to update incident'; + + handleUpdateIncidentMock.mockImplementation(() => { + throw new Error(errorMessage); + }); + + try { + await actionType.executor(executorOptions); + } catch (error) { + expect(error.message).toEqual(errorMessage); + } + }); +}); 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 new file mode 100644 index 0000000000000..01e566af17d08 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -0,0 +1,115 @@ +/* + * 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 { curry, isEmpty } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { + ActionType, + ActionTypeExecutorOptions, + ActionTypeExecutorResult, + ExecutorType, +} from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ServiceNow } from './lib'; + +import * as i18n from './translations'; + +import { ACTION_TYPE_ID } from './constants'; +import { ConfigType, SecretsType, ParamsType, CommentType } from './types'; + +import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; + +import { buildMap, mapParams } from './helpers'; +import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; + +function validateConfig( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ConfigType +) { + try { + if (isEmpty(configObject.casesConfiguration.mapping)) { + return i18n.MAPPING_EMPTY; + } + + configurationUtilities.ensureWhitelistedUri(configObject.apiUrl); + } catch (whitelistError) { + return i18n.WHITE_LISTED_ERROR(whitelistError.message); + } +} + +function validateSecrets( + configurationUtilities: ActionsConfigurationUtilities, + secrets: SecretsType +) {} + +// action type definition +export function getActionType({ + configurationUtilities, + executor = serviceNowExecutor, +}: { + configurationUtilities: ActionsConfigurationUtilities; + executor?: ExecutorType; +}): ActionType { + return { + id: ACTION_TYPE_ID, + name: i18n.NAME, + validate: { + config: schema.object(ConfigSchemaProps, { + validate: curry(validateConfig)(configurationUtilities), + }), + secrets: schema.object(SecretsSchemaProps, { + validate: curry(validateSecrets)(configurationUtilities), + }), + params: ParamsSchema, + }, + executor, + }; +} + +// action executor + +async function serviceNowExecutor( + execOptions: ActionTypeExecutorOptions +): Promise { + const actionId = execOptions.actionId; + const { + apiUrl, + casesConfiguration: { mapping }, + } = execOptions.config as ConfigType; + const { username, password } = execOptions.secrets as SecretsType; + const params = execOptions.params as ParamsType; + const { comments, incidentId, ...restParams } = params; + + const finalMap = buildMap(mapping); + const restParamsMapped = mapParams(restParams, finalMap); + const serviceNow = new ServiceNow({ url: apiUrl, username, password }); + + const handlerInput = { + serviceNow, + params: restParamsMapped, + comments: comments as CommentType[], + mapping: finalMap, + }; + + const res: Pick & + Pick = { + status: 'ok', + actionId, + }; + + let data = {}; + + if (!incidentId) { + data = await handleCreateIncident(handlerInput); + } else { + data = await handleUpdateIncident({ incidentId, ...handlerInput }); + } + + return { + ...res, + data, + }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts new file mode 100644 index 0000000000000..c84e1928e2e5a --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const API_VERSION = 'v2'; +export const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; +export const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; +export const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts new file mode 100644 index 0000000000000..22be625611e85 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import axios from 'axios'; +import { ServiceNow } from '.'; +import { instance, params } from '../mock'; + +jest.mock('axios'); + +axios.create = jest.fn(() => axios); +const axiosMock = (axios as unknown) as jest.Mock; + +let serviceNow: ServiceNow; + +const testMissingConfiguration = (field: string) => { + expect.assertions(1); + try { + new ServiceNow({ ...instance, [field]: '' }); + } catch (error) { + expect(error.message).toEqual('[Action][ServiceNow]: Wrong configuration.'); + } +}; + +const prependInstanceUrl = (url: string): string => `${instance.url}/${url}`; + +describe('ServiceNow lib', () => { + beforeEach(() => { + serviceNow = new ServiceNow(instance); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should thrown an error if url is missing', () => { + testMissingConfiguration('url'); + }); + + test('should thrown an error if username is missing', () => { + testMissingConfiguration('username'); + }); + + test('should thrown an error if password is missing', () => { + testMissingConfiguration('password'); + }); + + test('get user id', async () => { + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: [{ sys_id: '123' }] }, + }); + + const res = await serviceNow.getUserID(); + const [url, { method }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl('api/now/v2/table/sys_user?user_name=username')); + expect(method).toEqual('get'); + expect(res).toEqual('123'); + }); + + test('create incident', async () => { + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, + }); + + const res = await serviceNow.createIncident({ + short_description: 'A title', + description: 'A description', + caller_id: '123', + }); + const [url, { method, data }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident')); + expect(method).toEqual('post'); + expect(data).toEqual({ + short_description: 'A title', + description: 'A description', + caller_id: '123', + }); + + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('update incident', async () => { + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_id: '123', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, + }); + + const res = await serviceNow.updateIncident('123', { + short_description: params.title, + }); + const [url, { method, data }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); + expect(method).toEqual('patch'); + expect(data).toEqual({ short_description: params.title }); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('create comment', async () => { + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, + }); + + const comment = { + commentId: '456', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: undefined, + }; + + const res = await serviceNow.createComment('123', comment, 'comments'); + + const [url, { method, data }] = axiosMock.mock.calls[0]; + + expect(url).toEqual(prependInstanceUrl(`api/now/v2/table/incident/123`)); + expect(method).toEqual('patch'); + expect(data).toEqual({ + comments: 'A comment', + }); + + expect(res).toEqual({ + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + + test('create batch comment', async () => { + axiosMock.mockReturnValueOnce({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_updated_on: '2020-03-10 12:24:20' } }, + }); + + axiosMock.mockReturnValueOnce({ + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { result: { sys_updated_on: '2020-03-10 12:25:20' } }, + }); + + const comments = [ + { + commentId: '123', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: undefined, + }, + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'A second comment', + incidentCommentId: undefined, + }, + ]; + const res = await serviceNow.batchCreateComments('000', comments, 'comments'); + + comments.forEach((comment, index) => { + const [url, { method, data }] = axiosMock.mock.calls[index]; + expect(url).toEqual(prependInstanceUrl('api/now/v2/table/incident/000')); + expect(method).toEqual('patch'); + expect(data).toEqual({ + comments: comment.comment, + }); + expect(res).toEqual([ + { commentId: '123', pushedDate: '2020-03-10T12:24:20.000Z' }, + { commentId: '456', pushedDate: '2020-03-10T12:25:20.000Z' }, + ]); + }); + }); + + test('throw if not status is not ok', async () => { + expect.assertions(1); + + axiosMock.mockResolvedValue({ + status: 401, + headers: { + 'content-type': 'application/json', + }, + }); + try { + await serviceNow.getUserID(); + } catch (error) { + expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + } + }); + + test('throw if not content-type is not application/json', async () => { + expect.assertions(1); + + axiosMock.mockResolvedValue({ + status: 200, + headers: { + 'content-type': 'application/html', + }, + }); + try { + await serviceNow.getUserID(); + } catch (error) { + expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + } + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts new file mode 100644 index 0000000000000..b3d17affb14c2 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; + +import { INCIDENT_URL, USER_URL, COMMENT_URL } from './constants'; +import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types'; +import { CommentType } from '../types'; + +const validStatusCodes = [200, 201]; + +class ServiceNow { + private readonly incidentUrl: string; + private readonly commentUrl: string; + private readonly userUrl: string; + private readonly axios: AxiosInstance; + + constructor(private readonly instance: Instance) { + if ( + !this.instance || + !this.instance.url || + !this.instance.username || + !this.instance.password + ) { + throw Error('[Action][ServiceNow]: Wrong configuration.'); + } + + this.incidentUrl = `${this.instance.url}/${INCIDENT_URL}`; + this.commentUrl = `${this.instance.url}/${COMMENT_URL}`; + this.userUrl = `${this.instance.url}/${USER_URL}`; + this.axios = axios.create({ + auth: { username: this.instance.username, password: this.instance.password }, + }); + } + + private _throwIfNotAlive(status: number, contentType: string) { + if (!validStatusCodes.includes(status) || !contentType.includes('application/json')) { + throw new Error('[ServiceNow]: Instance is not alive.'); + } + } + + private async _request({ + url, + method = 'get', + data = {}, + }: { + url: string; + method?: Method; + data?: any; + }): Promise { + const res = await this.axios(url, { method, data }); + this._throwIfNotAlive(res.status, res.headers['content-type']); + return res; + } + + private _patch({ url, data }: { url: string; data: any }): Promise { + return this._request({ + url, + method: 'patch', + data, + }); + } + + private _addTimeZoneToDate(date: string, timezone = 'GMT'): string { + return `${date} GMT`; + } + + async getUserID(): Promise { + const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); + return res.data.result[0].sys_id; + } + + async createIncident(incident: Incident): Promise { + const res = await this._request({ + url: `${this.incidentUrl}`, + method: 'post', + data: { ...incident }, + }); + + return { + number: res.data.result.number, + incidentId: res.data.result.sys_id, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), + }; + } + + async updateIncident(incidentId: string, incident: UpdateIncident): Promise { + const res = await this._patch({ + url: `${this.incidentUrl}/${incidentId}`, + data: { ...incident }, + }); + + return { + number: res.data.result.number, + incidentId: res.data.result.sys_id, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } + + async batchCreateComments( + incidentId: string, + comments: CommentType[], + field: string + ): Promise { + const res = await Promise.all(comments.map(c => this.createComment(incidentId, c, field))); + return res; + } + + async createComment( + incidentId: string, + comment: CommentType, + field: string + ): Promise { + const res = await this._patch({ + url: `${this.commentUrl}/${incidentId}`, + data: { [field]: comment.comment }, + }); + + return { + commentId: comment.commentId, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } +} + +export { ServiceNow }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts new file mode 100644 index 0000000000000..4a3c5c42fcb44 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.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. + */ + +export interface Instance { + url: string; + username: string; + password: string; +} + +export interface Incident { + short_description?: string; + description?: string; + caller_id?: string; +} + +export interface IncidentResponse { + number: string; + incidentId: string; + pushedDate: string; +} + +export interface CommentResponse { + commentId: string; + pushedDate: string; +} + +export type UpdateIncident = Partial; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts new file mode 100644 index 0000000000000..9a150bbede5f8 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MapsType, FinalMapping, ParamsType } from './types'; +import { Incident } from './lib/types'; + +const mapping: MapsType[] = [ + { source: 'title', target: 'short_description', actionType: 'nothing' }, + { source: 'description', target: 'description', actionType: 'nothing' }, + { source: 'comments', target: 'comments', actionType: 'nothing' }, +]; + +const finalMapping: FinalMapping = new Map(); + +finalMapping.set('title', { + target: 'short_description', + actionType: 'nothing', +}); + +finalMapping.set('description', { + target: 'description', + actionType: 'nothing', +}); + +finalMapping.set('comments', { + target: 'comments', + actionType: 'nothing', +}); + +finalMapping.set('short_description', { + target: 'title', + actionType: 'nothing', +}); + +const params: ParamsType = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', + title: 'Incident title', + description: 'Incident description', + comments: [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'A comment', + incidentCommentId: '263ede42075300100e48fbbf7c1ed047', + }, + { + commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', + version: 'WlK3LDFd', + comment: 'Another comment', + incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', + }, + ], +}; + +const incidentResponse = { + incidentId: 'c816f79cc0a8016401c5a33be04be441', + number: 'INC0010001', +}; + +const userId = '2e9a0a5e2f79001016ab51172799b670'; + +const axiosResponse = { + status: 200, + headers: { + 'content-type': 'application/json', + }, +}; +const userIdResponse = { + result: [{ sys_id: userId }], +}; + +const incidentAxiosResponse = { + result: { sys_id: incidentResponse.incidentId, number: incidentResponse.number }, +}; + +const instance = { + url: 'https://instance.service-now.com', + username: 'username', + password: 'password', +}; + +const incident: Incident = { + short_description: params.title, + description: params.description, + caller_id: userId, +}; + +export { + mapping, + finalMapping, + params, + incidentResponse, + incidentAxiosResponse, + userId, + userIdResponse, + axiosResponse, + instance, + incident, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts new file mode 100644 index 0000000000000..0bb4f50819665 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +export const MapsSchema = schema.object({ + source: schema.string(), + target: schema.string(), + actionType: schema.oneOf([ + schema.literal('nothing'), + schema.literal('overwrite'), + schema.literal('append'), + ]), +}); + +export const CasesConfigurationSchema = schema.object({ + mapping: schema.arrayOf(MapsSchema), +}); + +export const ConfigSchemaProps = { + apiUrl: schema.string(), + casesConfiguration: CasesConfigurationSchema, +}; + +export const ConfigSchema = schema.object(ConfigSchemaProps); + +export const SecretsSchemaProps = { + password: schema.string(), + username: schema.string(), +}; + +export const SecretsSchema = schema.object(SecretsSchemaProps); + +export const CommentSchema = schema.object({ + commentId: schema.string(), + comment: schema.string(), + version: schema.maybe(schema.string()), + incidentCommentId: schema.maybe(schema.string()), +}); + +export const ExecutorAction = schema.oneOf([ + schema.literal('newIncident'), + schema.literal('updateIncident'), +]); + +export const ParamsSchema = schema.object({ + caseId: schema.string(), + comments: schema.maybe(schema.arrayOf(CommentSchema)), + description: schema.maybe(schema.string()), + title: schema.maybe(schema.string()), + incidentId: schema.maybe(schema.string()), +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts new file mode 100644 index 0000000000000..8601c5ce772db --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const API_URL_REQUIRED = i18n.translate( + 'xpack.actions.builtin.servicenow.servicenowApiNullError', + { + defaultMessage: 'ServiceNow [apiUrl] is required', + } +); + +export const WHITE_LISTED_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.servicenow.servicenowApiWhitelistError', { + defaultMessage: 'error configuring servicenow action: {message}', + values: { + message, + }, + }); + +export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { + defaultMessage: 'ServiceNow', +}); + +export const MAPPING_EMPTY = i18n.translate('xpack.actions.builtin.servicenow.emptyMapping', { + defaultMessage: '[casesConfiguration.mapping]: expected non-empty but got empty', +}); + +export const ERROR_POSTING = i18n.translate( + 'xpack.actions.builtin.servicenow.postingErrorMessage', + { + defaultMessage: 'error posting servicenow event', + } +); + +export const RETRY_POSTING = (status: number) => + i18n.translate('xpack.actions.builtin.servicenow.postingRetryErrorMessage', { + defaultMessage: 'error posting servicenow event: http status {status}, retry later', + values: { + status, + }, + }); + +export const UNEXPECTED_STATUS = (status: number) => + i18n.translate('xpack.actions.builtin.servicenow.postingUnexpectedErrorMessage', { + defaultMessage: 'error posting servicenow event: unexpected status {status}', + values: { + status, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts new file mode 100644 index 0000000000000..7442f14fed064 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -0,0 +1,56 @@ +/* + * 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 { TypeOf } from '@kbn/config-schema'; + +import { + ConfigSchema, + SecretsSchema, + ParamsSchema, + CasesConfigurationSchema, + MapsSchema, + CommentSchema, +} from './schema'; + +import { ServiceNow } from './lib'; + +// config definition +export type ConfigType = TypeOf; + +// secrets definition +export type SecretsType = TypeOf; + +export type ParamsType = TypeOf; + +export type CasesConfigurationType = TypeOf; +export type MapsType = TypeOf; +export type CommentType = TypeOf; + +export type FinalMapping = Map; + +export interface ActionHandlerArguments { + serviceNow: ServiceNow; + params: any; + comments: CommentType[]; + mapping: FinalMapping; +} + +export type UpdateParamsType = Partial; +export type UpdateActionHandlerArguments = ActionHandlerArguments & { + incidentId: string; +}; + +export interface IncidentCreationResponse { + incidentId: string; + number: string; + comments?: CommentsZipped[]; + pushedDate: string; +} + +export interface CommentsZipped { + commentId: string; + pushedDate: string; +} diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts index a872edfc17135..aeec07aba906c 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts @@ -29,7 +29,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] { const allPaths = Object.values(ExternalServiceSimulator).map(service => getExternalServiceSimulatorPath(service) ); - allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v1/table/incident`); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); return allPaths; } diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts index f215b63560339..3f1a095238939 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts @@ -9,8 +9,10 @@ import Hapi from 'hapi'; interface ServiceNowRequest extends Hapi.Request { payload: { - comments: string; - short_description: string; + caseId: string; + title?: string; + description?: string; + comments?: Array<{ commentId: string; version: string; comment: string }>; }; } export function initPlugin(server: Hapi.Server, path: string) { @@ -22,8 +24,16 @@ export function initPlugin(server: Hapi.Server, path: string) { validate: { options: { abortEarly: false }, payload: Joi.object().keys({ - comments: Joi.string(), - short_description: Joi.string(), + caseId: Joi.string(), + title: Joi.string(), + description: Joi.string(), + comments: Joi.array().items( + Joi.object({ + commentId: Joi.string(), + version: Joi.string(), + comment: Joi.string(), + }) + ), }), }, }, @@ -32,14 +42,46 @@ export function initPlugin(server: Hapi.Server, path: string) { server.route({ method: 'POST', - path: `${path}/api/now/v1/table/incident`, + path: `${path}/api/now/v2/table/incident`, options: { auth: false, validate: { options: { abortEarly: false }, payload: Joi.object().keys({ - comments: Joi.string(), - short_description: Joi.string(), + caseId: Joi.string(), + title: Joi.string(), + description: Joi.string(), + comments: Joi.array().items( + Joi.object({ + commentId: Joi.string(), + version: Joi.string(), + comment: Joi.string(), + }) + ), + }), + }, + }, + handler: servicenowHandler, + }); + + server.route({ + method: 'PATCH', + path: `${path}/api/now/v2/table/incident`, + options: { + auth: false, + validate: { + options: { abortEarly: false }, + payload: Joi.object().keys({ + caseId: Joi.string(), + title: Joi.string(), + description: Joi.string(), + comments: Joi.array().items( + Joi.object({ + commentId: Joi.string(), + version: Joi.string(), + comment: Joi.string(), + }) + ), }), }, }, @@ -51,61 +93,9 @@ export function initPlugin(server: Hapi.Server, path: string) { // more info. function servicenowHandler(request: ServiceNowRequest, h: any) { - const body = request.payload; - const text = body && body.short_description; - if (text == null) { - return jsonResponse(h, 400, 'bad request to servicenow simulator'); - } - - switch (text) { - case 'success': - return jsonResponse(h, 200, 'Success'); - - case 'created': - return jsonResponse(h, 201, 'Created'); - - case 'no_text': - return jsonResponse(h, 204, 'Success'); - - case 'invalid_payload': - return jsonResponse(h, 400, 'Bad Request'); - - case 'unauthorized': - return jsonResponse(h, 401, 'Unauthorized'); - - case 'forbidden': - return jsonResponse(h, 403, 'Forbidden'); - - case 'not_found': - return jsonResponse(h, 404, 'Not found'); - - case 'not_allowed': - return jsonResponse(h, 405, 'Method not allowed'); - - case 'not_acceptable': - return jsonResponse(h, 406, 'Not acceptable'); - - case 'unsupported': - return jsonResponse(h, 415, 'Unsupported media type'); - - case 'status_500': - return jsonResponse(h, 500, 'simulated servicenow 500 response'); - - case 'rate_limit': - const response = { - retry_after: 1, - ok: false, - error: 'rate_limited', - }; - - return h - .response(response) - .type('application/json') - .header('retry-after', '1') - .code(429); - } - - return jsonResponse(h, 400, 'unknown request to servicenow simulator'); + return jsonResponse(h, 200, { + result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' }, + }); } function jsonResponse(h: any, code: number, object?: any) { 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 15662649266ae..63c118966cfae 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 @@ -13,26 +13,60 @@ import { ExternalServiceSimulator, } from '../../../../common/fixtures/plugins/actions'; -// node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts +// node ../scripts/functional_test_runner.js --grep "servicenow" --config=test/alerting_api_integration/security_and_spaces/config.ts + +const mapping = [ + { + source: 'title', + target: 'description', + actionType: 'nothing', + }, + { + source: 'description', + target: 'short_description', + actionType: 'nothing', + }, + { + source: 'comments', + target: 'comments', + actionType: 'nothing', + }, +]; // eslint-disable-next-line import/no-default-export export default function servicenowTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', + casesConfiguration: { mapping: [...mapping] }, }, secrets: { password: 'elastic', username: 'changeme', }, params: { - comments: 'hello cool service now incident', - short_description: 'this is a cool service now incident', + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + title: 'A title', + description: 'A description', + comments: [ + { + commentId: '123', + version: 'WzU3LDFd', + comment: 'A comment', + }, + { + commentId: '456', + version: 'WzU5LVFd', + comment: 'Another comment', + }, + ], }, }; + describe('servicenow', () => { let simulatedActionId = ''; let servicenowSimulatorURL: string = ''; @@ -55,8 +89,9 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, + casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, }, - secrets: mockServiceNow.secrets, + secrets: { ...mockServiceNow.secrets }, }) .expect(200); @@ -66,6 +101,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, + casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, }, }); @@ -81,11 +117,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, + casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, }, }); }); - it('should respond with a 400 Bad Request when creating a servicenow action with no webhookUrl', async () => { + it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { await supertest .post('/api/action') .set('kbn-xsrf', 'foo') @@ -105,7 +142,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted webhookUrl', async () => { + it('should respond with a 400 Bad Request when creating a servicenow action with a non whitelisted apiUrl', async () => { await supertest .post('/api/action') .set('kbn-xsrf', 'foo') @@ -114,7 +151,9 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: 'http://servicenow.mynonexistent.com', + casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, }, + secrets: { ...mockServiceNow.secrets }, }) .expect(400) .then((resp: any) => { @@ -136,6 +175,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, + casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, }, }) .expect(400) @@ -149,123 +189,127 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); - it('should create our servicenow simulator action successfully', async () => { - const { body: createdSimulatedAction } = await supertest + it('should respond with a 400 Bad Request when creating a servicenow action without casesConfiguration', async () => { + await supertest .post('/api/action') .set('kbn-xsrf', 'foo') .send({ - name: 'A servicenow simulator', + name: 'A servicenow action', actionTypeId: '.servicenow', config: { apiUrl: servicenowSimulatorURL, }, - secrets: mockServiceNow.secrets, - }) - .expect(200); - - simulatedActionId = createdSimulatedAction.id; - }); - - it('should handle executing with a simulated success', async () => { - const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - comments: 'success', - short_description: 'success', - }, + secrets: { ...mockServiceNow.secrets }, }) - .expect(200); - - expect(result.status).to.eql('ok'); - }); - - it('should handle executing with a simulated success without comments', async () => { - const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - short_description: 'success', - }, - }) - .expect(200); - - expect(result.status).to.eql('ok'); + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', + }); + }); }); - it('should handle failing with a simulated success without short_description', async () => { + it('should respond with a 400 Bad Request when creating a servicenow action with empty mapping', async () => { await supertest - .post(`/api/action/${simulatedActionId}/_execute`) + .post('/api/action') .set('kbn-xsrf', 'foo') .send({ - params: { - comments: 'success', + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { mapping: [] }, }, + secrets: { ...mockServiceNow.secrets }, }) + .expect(400) .then((resp: any) => { expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, + statusCode: 400, + error: 'Bad Request', message: - 'error validating action params: [short_description]: expected value of type [string] but got [undefined]', + 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', }); }); }); - it('should handle a 40x servicenow error', async () => { - const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_execute`) + it('should respond with a 400 Bad Request when creating a servicenow action with wrong actionType', async () => { + await supertest + .post('/api/action') .set('kbn-xsrf', 'foo') .send({ - params: { - comments: 'invalid_payload', - short_description: 'invalid_payload', + name: 'A servicenow action', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'description', + actionType: 'non-supported', + }, + ], + }, }, + secrets: { ...mockServiceNow.secrets }, }) - .expect(200); - expect(result.status).to.equal('error'); - expect(result.message).to.match(/error posting servicenow event: unexpected status 400/); + .expect(400); }); - it('should handle a 429 servicenow error', async () => { - const { body: result } = await supertest - .post(`/api/action/${simulatedActionId}/_execute`) + it('should create our servicenow simulator action successfully', async () => { + const { body: createdSimulatedAction } = await supertest + .post('/api/action') .set('kbn-xsrf', 'foo') .send({ - params: { - comments: 'rate_limit', - short_description: 'rate_limit', + name: 'A servicenow simulator', + actionTypeId: '.servicenow', + config: { + apiUrl: servicenowSimulatorURL, + casesConfiguration: { ...mockServiceNow.config.casesConfiguration }, }, + secrets: { ...mockServiceNow.secrets }, }) .expect(200); - expect(result.status).to.equal('error'); - expect(result.message).to.equal( - 'error posting servicenow event: http status 429, retry later' - ); - expect(result.retry).to.equal(true); + simulatedActionId = createdSimulatedAction.id; }); - it('should handle a 500 servicenow error', async () => { + it('should handle executing with a simulated success', async () => { const { body: result } = await supertest .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { - comments: 'status_500', - short_description: 'status_500', - }, + params: { caseId: 'success' }, }) .expect(200); - expect(result.status).to.equal('error'); - expect(result.message).to.equal( - 'error posting servicenow event: http status 500, retry later' - ); - expect(result.retry).to.equal(true); + expect(result).to.eql({ + status: 'ok', + actionId: simulatedActionId, + data: { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z' }, + }); + }); + + it('should handle failing with a simulated success without caseId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [caseId]: expected value of type [string] but got [undefined]', + }); + }); }); }); } From 38717982e3a6dd93e44db84f1199acede5cab3ee Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 11 Mar 2020 15:18:49 +0100 Subject: [PATCH 10/13] Clean up .eslintrc.js with better variable naming (#59864) --- .eslintrc.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6155d182f7cd7..60d26cbfbab73 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -49,9 +49,9 @@ const ELASTIC_LICENSE_HEADER = ` */ `; -const allMochaRules = {}; +const allMochaRulesOff = {}; Object.keys(require('eslint-plugin-mocha').rules).forEach(k => { - allMochaRules['mocha/' + k] = 'off'; + allMochaRulesOff['mocha/' + k] = 'off'; }); module.exports = { @@ -524,7 +524,7 @@ module.exports = { */ { files: ['test/harden/*.js'], - rules: allMochaRules, + rules: allMochaRulesOff, }, /** From 40901496caee193a9c0424377a864d1efd7b88b2 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Wed, 11 Mar 2020 09:25:20 -0500 Subject: [PATCH 11/13] [DOCS] Removed Coordinate and Region Maps (#59828) * [DOCS] Removed Coordinate and Region Maps * Review comment * Added redirect for visualize-maps --- docs/redirects.asciidoc | 7 +- docs/setup/settings.asciidoc | 6 +- docs/user/visualize.asciidoc | 6 +- docs/visualize/tilemap.asciidoc | 110 ++------------------------------ 4 files changed, 13 insertions(+), 116 deletions(-) diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 8ad5330f3fda5..fd835bde83322 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -64,4 +64,9 @@ This page has moved. Please see <>. [role="exclude",id="tilemap"] == Coordinate map -This page has moved. Please see <>. +This page has moved. Please see <>. + +[role="exclude",id="visualize-maps"] +== Maps + +This page has moved. Please see <>. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 80d04c260e25f..71bb7b81ea420 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -220,15 +220,13 @@ requests. Supported on Elastic Cloud Enterprise. `map.includeElasticMapsService:`:: *Default: true* Set to false to disable connections to Elastic Maps Service. When `includeElasticMapsService` is turned off, only the vector layers configured by `map.regionmap` -and the tile layer configured by `map.tilemap.url` will be available in the <>, -<>, and <>. +and the tile layer configured by `map.tilemap.url` will be available in <>. `map.proxyElasticMapsServiceInMaps:`:: *Default: false* Set to true to proxy all <> Elastic Maps Service requests through the Kibana server. -This setting does not impact <> and <>. [[regionmap-settings]] `map.regionmap:`:: Specifies additional vector layers for -use in <> visualizations. Supported on {ece}. Each layer +use in <> visualizations. Supported on {ece}. Each layer object points to an external vector file that contains a geojson FeatureCollection. The file must use the https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system (ESPG:4326)] diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index f6be2040e3e8c..ebc2f404d43c1 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -40,11 +40,7 @@ data sets. <>:: * *<>* — Displays geospatial data in {kib}. -* *Coordinate map* — Displays points on a map using a geohash aggregation. - -* *Region map* — Merges any structured map data onto a shape. - -* *Heat map* — Displays shaded cells within a matrix. +* <>:: Display shaded cells within a matrix. <>:: diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc index 349fa681a9777..51342847080e0 100644 --- a/docs/visualize/tilemap.asciidoc +++ b/docs/visualize/tilemap.asciidoc @@ -1,116 +1,14 @@ -[[visualize-maps]] -== Maps - -To tell a story and answer questions about your geographical data, you can create several types of interactive maps with Visualize. - -Visualize supports the following maps: - -* *Coordinate* — Display latitude and longitude coordinates that are associated to the specified bucket aggregation. - -* *Region* — Display colored boundary vector shapes using a gradient. Darker colors indicate larger values, and lighter colors indicate smaller values. - -* *Heat* — Display graphical representations of data where the individual values are represented by colors. - -NOTE: The maps in Visualize have been replaced with <>, which offers more functionality. - -[float] -[[coordinate-map]] -=== Coordinate map - -Use a coordinate map when your data set includes latitude and longitude values. For example, use a coordinate map to see the varying popularity of destination airports using the sample flight data. - -[role="screenshot"] -image::images/visualize_coordinate_map_example.png[] - -[float] -[[build-coordinate-map]] -==== Build a coordinate map - -Configure the `kibana.yml` settings and add the aggregations. - -. Configure the following `kibana.yml` settings: - -* Set `xpack.maps.showMapVisualizationTypes` to `true`. - -* To use a tile service provider for coordinate maps other than https://www.elastic.co/elastic-maps-service[Elastic Maps Service], configure the <>. - -. To display your data on the coordinate map, use the following aggregations: - -* <> - -* <> - -. Specify the geohash bucket aggregation options: - -* *Precision* slider — Determines the granularity of the results displayed on the map. To show the *Precision* slider, deselect *Change precision on map zoom*. For information on the area specified by each precision level, refer to {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[geohash grid]. -+ -NOTE: Higher precisions increase memory usage for the browser that displays {kib} and the underlying -{es} cluster. - -* *Place markers off grid (use {ref}/search-aggregations-metrics-geocentroid-aggregation.html[geocentroid])* — When you selected, the markers are -placed in the center of all documents in the bucket, and a more accurate visualization is created. When deselected, the markers are placed in the center -of the geohash grid cell. -+ -NOTE: When you have multiple values in the geo_point, the coordinate map is unable to accurately calculate the geo_centroid. - -[float] -[[navigate-coordinate-map]] -==== Navigate the coordinate map - -To navigate the coordinate map, use the navigation options. - -* To move the map center, click and hold anywhere on the map and move the cursor. - -* To change the zoom level, click *Zoom In* or *Zoom out* image:images/viz-zoom.png[]. - -* To automatically crop the map boundaries to the -geohash buckets that have at least one result, click *Fit Data Bounds* image:images/viz-fit-bounds.png[]. - -[float] -[[region-map]] -=== Region map - -Use region maps when you want to show statistical data on a geographic area, such as a county, country, province, or state. For example, use a region map if you want to see the average sales for each country with the sample eCommerce order data. - -[role="screenshot"] -image::images/visualize_region_map_example.png[] - -[float] -[[build-region-maps]] -==== Build a region map - -Configure the `kibana.yml` settings and add the aggregations. - -. In `kibana.yml`, set `xpack.maps.showMapVisualizationTypes` to `true`. - -. To display your data on the region map, use the following aggregations: - -* <> -* <> -* <> - -[float] -[[navigate-region-map]] -==== Navigate the region map - -To navigate the region map, use the navigation options. - -* To change the zoom level, click *Zoom In* or *Zoom out* image:images/viz-zoom.png[]. - -* To automatically crop the map boundaries, click *Fit Data Bounds* image:images/viz-fit-bounds.png[]. - -[float] [[heat-map]] -=== Heat map +== Heat map -Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. +Display graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. [role="screenshot"] image::images/visualize_heat_map_example.png[] [float] [[build-heat-map]] -==== Build a heat map +=== Build a heat map To display your data on the heat map, use the supported aggregations. @@ -123,7 +21,7 @@ Heat maps support the following aggregations: [float] [[navigate-heatmap]] -==== Change the color ranges +=== Change the color ranges When only one color displays on the heat map, you might need to change the color ranges. From 2779d102a49810b9f5d8cbc18612f76af1627492 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Wed, 11 Mar 2020 08:55:34 -0700 Subject: [PATCH 12/13] [DOCS] Edits job creation UI text (#59830) --- .../create_analytics_form/create_analytics_form.tsx | 2 +- .../components/create_analytics_form/job_type.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index c744c357c9550..983375ecd4f61 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -610,7 +610,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.excludedFieldsHelpText', { defaultMessage: - 'Optionally select fields to be excluded from analysis. All other supported fields will be included', + 'Select fields to exclude from analysis. All other supported fields are included.', })} error={ excludesOptions.length === 0 && diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx index 988daac528fd7..ffed1ebf522f4 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx @@ -21,7 +21,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.outlierDetectionHelpText', { defaultMessage: - 'Outlier detection jobs require a source index that is mapped as a table-like data structure and will only analyze numeric and boolean fields. Please use the advanced editor to add custom options to the configuration.', + 'Outlier detection jobs require a source index that is mapped as a table-like data structure and analyze only numeric and boolean fields. Use the advanced editor to add custom options to the configuration.', } ); @@ -29,7 +29,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.outlierRegressionHelpText', { defaultMessage: - 'Regression jobs will only analyze numeric fields. Please use the advanced editor to apply custom options such as the prediction field name.', + 'Regression jobs analyze only numeric fields. Use the advanced editor to apply custom options, such as the prediction field name.', } ); @@ -37,7 +37,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.classificationHelpText', { defaultMessage: - 'Classification jobs require a source index that is mapped as a table-like data structure and supports fields that are numeric, boolean, text, keyword or ip. Please use the advanced editor to apply custom options such as the prediction field name.', + 'Classification jobs require a source index that is mapped as a table-like data structure and support fields that are numeric, boolean, text, keyword, or ip. Use the advanced editor to apply custom options, such as the prediction field name.', } ); From 66455316bb81ad4119f3c2af5939ab87c96b9397 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Wed, 11 Mar 2020 16:03:27 +0000 Subject: [PATCH 13/13] Clear out old rollup strategy (#59423) * Old search strategy cleanup * restore rollup strategy * Remove hasSearchStategyForIndexPattern * ts * fix jest tests * cleanup exports * pass index pattern type to server for rollups * merge fix * Fix types * ts fixes * oss strategy error handling * update translations * jest test fix * Use indexType instead of index * code review 1 * updated docs * jest test * jest snapshot Co-authored-by: Elastic Machine --- ...n-plugins-data-public.addsearchstrategy.md | 11 -- ...ugins-data-public.defaultsearchstrategy.md | 11 -- ...ns-data-public.essearchstrategyprovider.md | 11 -- ...gin-plugins-data-public.getdefaultquery.md | 28 ++++ ...-public.hassearchstategyforindexpattern.md | 11 -- ...-data-public.iessearchrequest.indextype.md | 11 ++ ...in-plugins-data-public.iessearchrequest.md | 1 + .../kibana-plugin-plugins-data-public.md | 6 - ...public.savedqueryattributes.description.md | 11 -- ...ata-public.savedqueryattributes.filters.md | 11 -- ...lugins-data-public.savedqueryattributes.md | 22 --- ...-data-public.savedqueryattributes.query.md | 11 -- ...-public.savedqueryattributes.timefilter.md | 11 -- ...-data-public.savedqueryattributes.title.md | 11 -- ...ins-data-public.timefiltersetup.history.md | 11 -- ...gin-plugins-data-public.timefiltersetup.md | 20 --- ...-data-public.timefiltersetup.timefilter.md | 11 -- src/legacy/core_plugins/data/public/index.ts | 6 +- src/legacy/core_plugins/data/public/plugin.ts | 10 +- .../public/control/create_search_source.ts | 4 +- .../public/control/list_control_factory.ts | 4 +- .../public/control/range_control_factory.ts | 4 +- .../kibana/public/discover/kibana_services.ts | 1 - .../np_ready/angular/directives/index.js | 5 - .../directives/unsupported_index_pattern.js | 49 ------ .../discover/np_ready/angular/discover.html | 5 - .../discover/np_ready/angular/discover.js | 14 +- .../data/common/search/es_search/types.ts | 1 + src/plugins/data/public/index.ts | 6 - src/plugins/data/public/public.api.md | 48 +----- .../saved_query/saved_query_service.test.ts | 2 +- .../public/search/fetch/call_client.test.ts | 87 ++++------ .../data/public/search/fetch/call_client.ts | 39 ++--- src/plugins/data/public/search/index.ts | 9 +- .../search/search_source/search_source.ts | 11 +- .../default_search_strategy.ts | 4 +- .../public/search/search_strategy/index.ts | 7 - .../search_strategy_registry.test.ts | 149 ------------------ .../search_strategy_registry.ts | 71 --------- .../ui/saved_query_form/save_query_form.tsx | 3 +- .../search/es_search/es_search_strategy.ts | 7 + src/plugins/data/server/search/routes.ts | 4 +- .../application/util/dependency_cache.ts | 4 +- x-pack/legacy/plugins/rollup/public/legacy.ts | 2 - x-pack/legacy/plugins/rollup/public/plugin.ts | 6 +- .../server/search/es_search_strategy.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 48 files changed, 129 insertions(+), 646 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.addsearchstrategy.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.defaultsearchstrategy.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.essearchstrategyprovider.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getdefaultquery.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.indextype.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.description.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.filters.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.query.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.title.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.history.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md delete mode 100644 src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/unsupported_index_pattern.js delete mode 100644 src/plugins/data/public/search/search_strategy/search_strategy_registry.test.ts delete mode 100644 src/plugins/data/public/search/search_strategy/search_strategy_registry.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.addsearchstrategy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.addsearchstrategy.md deleted file mode 100644 index 119e7fbe62536..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.addsearchstrategy.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [addSearchStrategy](./kibana-plugin-plugins-data-public.addsearchstrategy.md) - -## addSearchStrategy variable - -Signature: - -```typescript -addSearchStrategy: (searchStrategy: SearchStrategyProvider) => void -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.defaultsearchstrategy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.defaultsearchstrategy.md deleted file mode 100644 index d6a71cf561bc2..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.defaultsearchstrategy.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [defaultSearchStrategy](./kibana-plugin-plugins-data-public.defaultsearchstrategy.md) - -## defaultSearchStrategy variable - -Signature: - -```typescript -defaultSearchStrategy: SearchStrategyProvider -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.essearchstrategyprovider.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.essearchstrategyprovider.md deleted file mode 100644 index 1394c6b868546..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.essearchstrategyprovider.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [esSearchStrategyProvider](./kibana-plugin-plugins-data-public.essearchstrategyprovider.md) - -## esSearchStrategyProvider variable - -Signature: - -```typescript -esSearchStrategyProvider: TSearchStrategyProvider -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getdefaultquery.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getdefaultquery.md new file mode 100644 index 0000000000000..5e6627880333e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getdefaultquery.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [getDefaultQuery](./kibana-plugin-plugins-data-public.getdefaultquery.md) + +## getDefaultQuery() function + +Signature: + +```typescript +export declare function getDefaultQuery(language?: QueryLanguage): { + query: string; + language: QueryLanguage; +}; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| language | QueryLanguage | | + +Returns: + +`{ + query: string; + language: QueryLanguage; +}` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md deleted file mode 100644 index 94608e7a86820..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [hasSearchStategyForIndexPattern](./kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md) - -## hasSearchStategyForIndexPattern variable - -Signature: - -```typescript -hasSearchStategyForIndexPattern: (indexPattern: IndexPattern) => boolean -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.indextype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.indextype.md new file mode 100644 index 0000000000000..55b43efc52305 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.indextype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) > [indexType](./kibana-plugin-plugins-data-public.iessearchrequest.indextype.md) + +## IEsSearchRequest.indexType property + +Signature: + +```typescript +indexType?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.md index 7a40725a67e5f..ed24ca613cdf6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iessearchrequest.md @@ -14,5 +14,6 @@ export interface IEsSearchRequest extends IKibanaSearchRequest | Property | Type | Description | | --- | --- | --- | +| [indexType](./kibana-plugin-plugins-data-public.iessearchrequest.indextype.md) | string | | | [params](./kibana-plugin-plugins-data-public.iessearchrequest.params.md) | SearchParams | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 4b85461e64097..ce1375d277b75 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -74,32 +74,26 @@ | [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) | | | [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) | | | [SavedQuery](./kibana-plugin-plugins-data-public.savedquery.md) | | -| [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) | | | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | | | [SearchStrategyProvider](./kibana-plugin-plugins-data-public.searchstrategyprovider.md) | | -| [TimefilterSetup](./kibana-plugin-plugins-data-public.timefiltersetup.md) | | | [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) | | ## Variables | Variable | Description | | --- | --- | -| [addSearchStrategy](./kibana-plugin-plugins-data-public.addsearchstrategy.md) | | | [baseFormattersPublic](./kibana-plugin-plugins-data-public.baseformatterspublic.md) | | | [castEsToKbnFieldTypeName](./kibana-plugin-plugins-data-public.castestokbnfieldtypename.md) | Get the KbnFieldType name for an esType string | | [connectToQueryState](./kibana-plugin-plugins-data-public.connecttoquerystate.md) | Helper to setup two-way syncing of global data and a state container | | [createSavedQueryService](./kibana-plugin-plugins-data-public.createsavedqueryservice.md) | | -| [defaultSearchStrategy](./kibana-plugin-plugins-data-public.defaultsearchstrategy.md) | | | [ES\_SEARCH\_STRATEGY](./kibana-plugin-plugins-data-public.es_search_strategy.md) | | | [esFilters](./kibana-plugin-plugins-data-public.esfilters.md) | | | [esKuery](./kibana-plugin-plugins-data-public.eskuery.md) | | | [esQuery](./kibana-plugin-plugins-data-public.esquery.md) | | -| [esSearchStrategyProvider](./kibana-plugin-plugins-data-public.essearchstrategyprovider.md) | | | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | | [FilterBar](./kibana-plugin-plugins-data-public.filterbar.md) | | | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | -| [hasSearchStategyForIndexPattern](./kibana-plugin-plugins-data-public.hassearchstategyforindexpattern.md) | | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | | [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | | | [SearchBar](./kibana-plugin-plugins-data-public.searchbar.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.description.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.description.md deleted file mode 100644 index 859935480357c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.description.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [description](./kibana-plugin-plugins-data-public.savedqueryattributes.description.md) - -## SavedQueryAttributes.description property - -Signature: - -```typescript -description: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.filters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.filters.md deleted file mode 100644 index c2c1ac681802b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.filters.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [filters](./kibana-plugin-plugins-data-public.savedqueryattributes.filters.md) - -## SavedQueryAttributes.filters property - -Signature: - -```typescript -filters?: Filter[]; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.md deleted file mode 100644 index 612be6a1dabc6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) - -## SavedQueryAttributes interface - -Signature: - -```typescript -export interface SavedQueryAttributes -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [description](./kibana-plugin-plugins-data-public.savedqueryattributes.description.md) | string | | -| [filters](./kibana-plugin-plugins-data-public.savedqueryattributes.filters.md) | Filter[] | | -| [query](./kibana-plugin-plugins-data-public.savedqueryattributes.query.md) | Query | | -| [timefilter](./kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md) | SavedQueryTimeFilter | | -| [title](./kibana-plugin-plugins-data-public.savedqueryattributes.title.md) | string | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.query.md deleted file mode 100644 index 96673fc3a8fde..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.query.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [query](./kibana-plugin-plugins-data-public.savedqueryattributes.query.md) - -## SavedQueryAttributes.query property - -Signature: - -```typescript -query: Query; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md deleted file mode 100644 index b4edb059a3dfd..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [timefilter](./kibana-plugin-plugins-data-public.savedqueryattributes.timefilter.md) - -## SavedQueryAttributes.timefilter property - -Signature: - -```typescript -timefilter?: SavedQueryTimeFilter; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.title.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.title.md deleted file mode 100644 index 99ae1b83e8834..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.savedqueryattributes.title.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SavedQueryAttributes](./kibana-plugin-plugins-data-public.savedqueryattributes.md) > [title](./kibana-plugin-plugins-data-public.savedqueryattributes.title.md) - -## SavedQueryAttributes.title property - -Signature: - -```typescript -title: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.history.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.history.md deleted file mode 100644 index b2ef4a92c5fef..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.history.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimefilterSetup](./kibana-plugin-plugins-data-public.timefiltersetup.md) > [history](./kibana-plugin-plugins-data-public.timefiltersetup.history.md) - -## TimefilterSetup.history property - -Signature: - -```typescript -history: TimeHistoryContract; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.md deleted file mode 100644 index 3375b415e923b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimefilterSetup](./kibana-plugin-plugins-data-public.timefiltersetup.md) - -## TimefilterSetup interface - - -Signature: - -```typescript -export interface TimefilterSetup -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [history](./kibana-plugin-plugins-data-public.timefiltersetup.history.md) | TimeHistoryContract | | -| [timefilter](./kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md) | TimefilterContract | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md deleted file mode 100644 index 897ace53a282d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [TimefilterSetup](./kibana-plugin-plugins-data-public.timefiltersetup.md) > [timefilter](./kibana-plugin-plugins-data-public.timefiltersetup.timefilter.md) - -## TimefilterSetup.timefilter property - -Signature: - -```typescript -timefilter: TimefilterContract; -``` diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 9187e207ed0d6..5ced7bee10b7d 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -28,11 +28,7 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart } from './plugin'; -export { - SavedQueryAttributes, - SavedQuery, - SavedQueryTimeFilter, -} from '../../../../plugins/data/public'; +export { SavedQuery, SavedQueryTimeFilter } from '../../../../plugins/data/public'; export { // agg_types AggParam, // only the type is used externally, only in vis editor diff --git a/src/legacy/core_plugins/data/public/plugin.ts b/src/legacy/core_plugins/data/public/plugin.ts index 18230646ab412..f40cda8760bc7 100644 --- a/src/legacy/core_plugins/data/public/plugin.ts +++ b/src/legacy/core_plugins/data/public/plugin.ts @@ -18,12 +18,7 @@ */ import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; -import { - DataPublicPluginStart, - addSearchStrategy, - defaultSearchStrategy, - DataPublicPluginSetup, -} from '../../../../plugins/data/public'; +import { DataPublicPluginStart, DataPublicPluginSetup } from '../../../../plugins/data/public'; import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { @@ -111,9 +106,6 @@ export class DataPlugin public setup(core: CoreSetup, { data, uiActions }: DataPluginSetupDependencies) { setInjectedMetadata(core.injectedMetadata); - // This is to be deprecated once we switch to the new search service fully - addSearchStrategy(defaultSearchStrategy); - uiActions.attachAction( SELECT_RANGE_TRIGGER, selectRangeAction(data.query.filterManager, data.query.timefilter.timefilter) diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts index f792796230757..f238a2287ecdb 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PhraseFilter, IndexPattern, TimefilterSetup } from '../../../../../plugins/data/public'; +import { PhraseFilter, IndexPattern, TimefilterContract } from '../../../../../plugins/data/public'; import { SearchSource as SearchSourceClass, SearchSourceFields } from '../legacy_imports'; export function createSearchSource( @@ -27,7 +27,7 @@ export function createSearchSource( aggs: any, useTimeFilter: boolean, filters: PhraseFilter[] = [], - timefilter: TimefilterSetup['timefilter'] + timefilter: TimefilterContract ) { const searchSource = initialState ? new SearchSource(initialState) : new SearchSource(); // Do not not inherit from rootSearchSource to avoid picking up time and globals diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts index 56b42f295ce15..8364c82efecdb 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts @@ -26,7 +26,7 @@ import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterSetup } from '../../../../../plugins/data/public'; +import { IFieldType, TimefilterContract } from '../../../../../plugins/data/public'; function getEscapedQuery(query = '') { // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators @@ -74,7 +74,7 @@ const termsAgg = ({ field, size, direction, query }: TermsAggArgs) => { export class ListControl extends Control { private getInjectedVar: InputControlVisDependencies['core']['injectedMetadata']['getInjectedVar']; - private timefilter: TimefilterSetup['timefilter']; + private timefilter: TimefilterContract; abortController?: AbortController; lastAncestorValues: any; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts index b9191436b5968..d9b43c9dff201 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts +++ b/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts @@ -26,7 +26,7 @@ import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { IFieldType, TimefilterSetup } from '../.../../../../../../plugins/data/public'; +import { IFieldType, TimefilterContract } from '../.../../../../../../plugins/data/public'; const minMaxAgg = (field?: IFieldType) => { const aggBody: any = {}; @@ -52,7 +52,7 @@ const minMaxAgg = (field?: IFieldType) => { }; export class RangeControl extends Control { - timefilter: TimefilterSetup['timefilter']; + timefilter: TimefilterContract; abortController: any; min: any; max: any; diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 6947d985be436..8a8b5d8e0e3ea 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -70,7 +70,6 @@ export { IIndexPattern, IndexPattern, indexPatterns, - hasSearchStategyForIndexPattern, IFieldType, SearchSource, ISearchSource, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js index 1a3922dfc2008..5482258e06564 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/index.js @@ -19,7 +19,6 @@ import { DiscoverNoResults } from './no_results'; import { DiscoverUninitialized } from './uninitialized'; -import { DiscoverUnsupportedIndexPattern } from './unsupported_index_pattern'; import { DiscoverHistogram } from './histogram'; import { getAngularModule, wrapInI18nContext } from '../../../kibana_services'; @@ -33,8 +32,4 @@ app.directive('discoverUninitialized', reactDirective => reactDirective(wrapInI18nContext(DiscoverUninitialized)) ); -app.directive('discoverUnsupportedIndexPattern', reactDirective => - reactDirective(wrapInI18nContext(DiscoverUnsupportedIndexPattern), ['unsupportedType']) -); - app.directive('discoverHistogram', reactDirective => reactDirective(DiscoverHistogram)); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/unsupported_index_pattern.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/unsupported_index_pattern.js deleted file mode 100644 index 7cf4fd1d14181..0000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/directives/unsupported_index_pattern.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { Fragment } from 'react'; -import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const DiscoverUnsupportedIndexPattern = ({ unsupportedType }) => { - // This message makes the assumption that X-Pack will support this type, as is the case with - // rollup index patterns. - const message = ( - - ); - - return ( - - - - - - - - - - ); -}; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html index 2d44b12989228..2334e33deadba 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.html @@ -38,11 +38,6 @@

{{screenTitle}}

- - extends IKibanaSearchResponse { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 7487f048525bd..7d739103eb2bb 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -286,11 +286,7 @@ export { export { ES_SEARCH_STRATEGY, SYNC_SEARCH_STRATEGY, - defaultSearchStrategy, - esSearchStrategyProvider, getEsPreference, - addSearchStrategy, - hasSearchStategyForIndexPattern, getSearchErrorType, ISearchContext, TSearchStrategyProvider, @@ -348,9 +344,7 @@ export { SavedQuery, SavedQueryService, SavedQueryTimeFilter, - SavedQueryAttributes, InputTimeRange, - TimefilterSetup, TimeHistory, TimefilterContract, TimeHistoryContract, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index c41a4ef531443..48125254ff749 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -49,11 +49,6 @@ import { UiSettingsParams } from 'src/core/server/types'; import { UnregisterCallback } from 'history'; import { UserProvidedValues } from 'src/core/server/types'; -// Warning: (ae-missing-release-tag) "addSearchStrategy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const addSearchStrategy: (searchStrategy: SearchStrategyProvider) => void; - // Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -153,11 +148,6 @@ export interface DataPublicPluginStart { }; } -// Warning: (ae-missing-release-tag) "defaultSearchStrategy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const defaultSearchStrategy: SearchStrategyProvider; - // @public (undocumented) export enum ES_FIELD_TYPES { // (undocumented) @@ -313,11 +303,6 @@ export interface EsQueryConfig { // @public (undocumented) export type EsQuerySortValue = Record; -// Warning: (ae-missing-release-tag) "esSearchStrategyProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const esSearchStrategyProvider: TSearchStrategyProvider; - // Warning: (ae-missing-release-tag) "ExistsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -488,11 +473,6 @@ export function getSearchErrorType({ message }: Pick): " // @public (undocumented) export function getTime(indexPattern: IIndexPattern | undefined, timeRange: TimeRange, forceNow?: Date): import("../..").RangeFilter | undefined; -// Warning: (ae-missing-release-tag) "hasSearchStategyForIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const hasSearchStategyForIndexPattern: (indexPattern: IndexPattern) => boolean; - // Warning: (ae-missing-release-tag) "IDataPluginServices" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -517,6 +497,8 @@ export interface IDataPluginServices extends Partial { // // @public (undocumented) export interface IEsSearchRequest extends IKibanaSearchRequest { + // (undocumented) + indexType?: string; // (undocumented) params: SearchParams; } @@ -1227,28 +1209,14 @@ export interface RefreshInterval { // // @public (undocumented) export interface SavedQuery { + // Warning: (ae-forgotten-export) The symbol "SavedQueryAttributes" needs to be exported by the entry point index.d.ts + // // (undocumented) attributes: SavedQueryAttributes; // (undocumented) id: string; } -// Warning: (ae-missing-release-tag) "SavedQueryAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface SavedQueryAttributes { - // (undocumented) - description: string; - // (undocumented) - filters?: Filter[]; - // (undocumented) - query: Query; - // (undocumented) - timefilter?: SavedQueryTimeFilter; - // (undocumented) - title: string; -} - // Warning: (ae-missing-release-tag) "SavedQueryService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1484,14 +1452,6 @@ export const syncQueryStateWithUrl: (query: Pick<{ // @public (undocumented) export type TimefilterContract = PublicMethodsOf; -// @public (undocumented) -export interface TimefilterSetup { - // (undocumented) - history: TimeHistoryContract; - // (undocumented) - timefilter: TimefilterContract; -} - // Warning: (ae-missing-release-tag) "TimeHistory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts index c983cc4ea8fc5..a86a5b4ed401e 100644 --- a/src/plugins/data/public/query/saved_query/saved_query_service.test.ts +++ b/src/plugins/data/public/query/saved_query/saved_query_service.test.ts @@ -18,8 +18,8 @@ */ import { createSavedQueryService } from './saved_query_service'; -import { SavedQueryAttributes } from '../..'; import { FilterStateStore } from '../../../common'; +import { SavedQueryAttributes } from './types'; const savedQueryAttributes: SavedQueryAttributes = { title: 'foo', diff --git a/src/plugins/data/public/search/fetch/call_client.test.ts b/src/plugins/data/public/search/fetch/call_client.test.ts index 6b43157aab83b..7a99b7c064515 100644 --- a/src/plugins/data/public/search/fetch/call_client.test.ts +++ b/src/plugins/data/public/search/fetch/call_client.test.ts @@ -20,60 +20,35 @@ import { callClient } from './call_client'; import { handleResponse } from './handle_response'; import { FetchHandlers } from './types'; -import { SearchRequest } from '../..'; -import { SearchStrategySearchParams } from '../search_strategy'; - -const mockResponses = [{}, {}]; -const mockAbortFns = [jest.fn(), jest.fn()]; -const mockSearchFns = [ - jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ - searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[0])), - abort: mockAbortFns[0], - })), - jest.fn(({ searchRequests }: SearchStrategySearchParams) => ({ - searching: Promise.resolve(Array(searchRequests.length).fill(mockResponses[1])), - abort: mockAbortFns[1], - })), -]; -const mockSearchStrategies = mockSearchFns.map((search, i) => ({ search, id: i })); +import { SearchStrategySearchParams, defaultSearchStrategy } from '../search_strategy'; +const mockAbortFn = jest.fn(); jest.mock('./handle_response', () => ({ handleResponse: jest.fn((request, response) => response), })); -jest.mock('../search_strategy', () => ({ - getSearchStrategyForSearchRequest: (request: SearchRequest) => - mockSearchStrategies[request._searchStrategyId], - getSearchStrategyById: (id: number) => mockSearchStrategies[id], -})); +jest.mock('../search_strategy', () => { + return { + defaultSearchStrategy: { + search: jest.fn(({ searchRequests }: SearchStrategySearchParams) => { + return { + searching: Promise.resolve( + searchRequests.map(req => { + return { + id: req._searchStrategyId, + }; + }) + ), + abort: mockAbortFn, + }; + }), + }, + }; +}); describe('callClient', () => { beforeEach(() => { (handleResponse as jest.Mock).mockClear(); - mockAbortFns.forEach(fn => fn.mockClear()); - mockSearchFns.forEach(fn => fn.mockClear()); - }); - - test('Executes each search strategy with its group of matching requests', () => { - const searchRequests = [ - { _searchStrategyId: 0 }, - { _searchStrategyId: 1 }, - { _searchStrategyId: 0 }, - { _searchStrategyId: 1 }, - ]; - - callClient(searchRequests, [], {} as FetchHandlers); - - expect(mockSearchFns[0]).toBeCalled(); - expect(mockSearchFns[0].mock.calls[0][0].searchRequests).toEqual([ - searchRequests[0], - searchRequests[2], - ]); - expect(mockSearchFns[1]).toBeCalled(); - expect(mockSearchFns[1].mock.calls[0][0].searchRequests).toEqual([ - searchRequests[1], - searchRequests[3], - ]); }); test('Passes the additional arguments it is given to the search strategy', () => { @@ -82,8 +57,11 @@ describe('callClient', () => { callClient(searchRequests, [], args); - expect(mockSearchFns[0]).toBeCalled(); - expect(mockSearchFns[0].mock.calls[0][0]).toEqual({ searchRequests, ...args }); + expect(defaultSearchStrategy.search).toBeCalled(); + expect((defaultSearchStrategy.search as any).mock.calls[0][0]).toEqual({ + searchRequests, + ...args, + }); }); test('Returns the responses in the original order', async () => { @@ -91,7 +69,8 @@ describe('callClient', () => { const responses = await Promise.all(callClient(searchRequests, [], {} as FetchHandlers)); - expect(responses).toEqual([mockResponses[1], mockResponses[0]]); + expect(responses[0]).toEqual({ id: searchRequests[0]._searchStrategyId }); + expect(responses[1]).toEqual({ id: searchRequests[1]._searchStrategyId }); }); test('Calls handleResponse with each request and response', async () => { @@ -101,8 +80,12 @@ describe('callClient', () => { await Promise.all(responses); expect(handleResponse).toBeCalledTimes(2); - expect(handleResponse).toBeCalledWith(searchRequests[0], mockResponses[0]); - expect(handleResponse).toBeCalledWith(searchRequests[1], mockResponses[1]); + expect(handleResponse).toBeCalledWith(searchRequests[0], { + id: searchRequests[0]._searchStrategyId, + }); + expect(handleResponse).toBeCalledWith(searchRequests[1], { + id: searchRequests[1]._searchStrategyId, + }); }); test('If passed an abortSignal, calls abort on the strategy if the signal is aborted', () => { @@ -117,7 +100,7 @@ describe('callClient', () => { callClient(searchRequests, requestOptions, {} as FetchHandlers); abortController.abort(); - expect(mockAbortFns[0]).toBeCalled(); - expect(mockAbortFns[1]).not.toBeCalled(); + expect(mockAbortFn).toBeCalled(); + // expect(mockAbortFns[1]).not.toBeCalled(); }); }); diff --git a/src/plugins/data/public/search/fetch/call_client.ts b/src/plugins/data/public/search/fetch/call_client.ts index 6cc58b05ea183..b3c4c682fa60c 100644 --- a/src/plugins/data/public/search/fetch/call_client.ts +++ b/src/plugins/data/public/search/fetch/call_client.ts @@ -17,10 +17,9 @@ * under the License. */ -import { groupBy } from 'lodash'; import { handleResponse } from './handle_response'; import { FetchOptions, FetchHandlers } from './types'; -import { getSearchStrategyForSearchRequest, getSearchStrategyById } from '../search_strategy'; +import { defaultSearchStrategy } from '../search_strategy'; import { SearchRequest } from '..'; export function callClient( @@ -34,34 +33,18 @@ export function callClient( FetchOptions ]> = searchRequests.map((request, i) => [request, requestsOptions[i]]); const requestOptionsMap = new Map(requestOptionEntries); - - // Group the requests by the strategy used to search that specific request - const searchStrategyMap = groupBy(searchRequests, (request, i) => { - const searchStrategy = getSearchStrategyForSearchRequest(request, requestsOptions[i]); - return searchStrategy.id; - }); - - // Execute each search strategy with the group of requests, but return the responses in the same - // order in which they were received. We use a map to correlate the original request with its - // response. const requestResponseMap = new Map(); - Object.keys(searchStrategyMap).forEach(searchStrategyId => { - const searchStrategy = getSearchStrategyById(searchStrategyId); - const requests = searchStrategyMap[searchStrategyId]; - // There's no way `searchStrategy` could be undefined here because if we didn't get a matching strategy for this ID - // then an error would have been thrown above - const { searching, abort } = searchStrategy!.search({ - searchRequests: requests, - ...fetchHandlers, - }); + const { searching, abort } = defaultSearchStrategy.search({ + searchRequests, + ...fetchHandlers, + }); - requests.forEach((request, i) => { - const response = searching.then(results => handleResponse(request, results[i])); - const { abortSignal = null } = requestOptionsMap.get(request) || {}; - if (abortSignal) abortSignal.addEventListener('abort', abort); - requestResponseMap.set(request, response); - }); - }, []); + searchRequests.forEach((request, i) => { + const response = searching.then(results => handleResponse(request, results[i])); + const { abortSignal = null } = requestOptionsMap.get(request) || {}; + if (abortSignal) abortSignal.addEventListener('abort', abort); + requestResponseMap.set(request, response); + }); return searchRequests.map(request => requestResponseMap.get(request)); } diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index 2a54cfe2be785..6ccd90c6a9eff 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -42,14 +42,7 @@ export { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search export { LegacyApiCaller, SearchRequest, SearchResponse } from './es_client'; -export { - addSearchStrategy, - hasSearchStategyForIndexPattern, - defaultSearchStrategy, - SearchError, - SearchStrategyProvider, - getSearchErrorType, -} from './search_strategy'; +export { SearchError, SearchStrategyProvider, getSearchErrorType } from './search_strategy'; export { ISearchSource, diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 21e5ded6983ac..0c3321f03dabc 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -73,7 +73,7 @@ import _ from 'lodash'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; -import { SearchRequest } from '../..'; +import { IIndexPattern, SearchRequest } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; import { fetchSoon, FetchOptions, RequestFailure } from '../fetch'; @@ -339,11 +339,20 @@ export class SearchSource { return searchRequest; } + private getIndexType(index: IIndexPattern) { + if (this.searchStrategyId) { + return this.searchStrategyId === 'default' ? undefined : this.searchStrategyId; + } else { + return index?.type; + } + } + private flatten() { const searchRequest = this.mergeProps(); searchRequest.body = searchRequest.body || {}; const { body, index, fields, query, filters, highlightAll } = searchRequest; + searchRequest.indexType = this.getIndexType(index); const computedFields = index ? index.getComputedFields() : {}; diff --git a/src/plugins/data/public/search/search_strategy/default_search_strategy.ts b/src/plugins/data/public/search/search_strategy/default_search_strategy.ts index 6fcb1e6b3e8d2..2bd88f51587a8 100644 --- a/src/plugins/data/public/search/search_strategy/default_search_strategy.ts +++ b/src/plugins/data/public/search/search_strategy/default_search_strategy.ts @@ -74,7 +74,7 @@ function search({ }: SearchStrategySearchParams) { const abortController = new AbortController(); const searchParams = getSearchParams(config, esShardTimeout); - const promises = searchRequests.map(({ index, body }) => { + const promises = searchRequests.map(({ index, indexType, body }) => { const params = { index: index.title || index, body, @@ -82,7 +82,7 @@ function search({ }; const { signal } = abortController; return searchService - .search({ params }, { signal }) + .search({ params, indexType }, { signal }) .toPromise() .then(({ rawResponse }) => rawResponse); }); diff --git a/src/plugins/data/public/search/search_strategy/index.ts b/src/plugins/data/public/search/search_strategy/index.ts index 330e10d7d30e4..e3de2ea46e3ec 100644 --- a/src/plugins/data/public/search/search_strategy/index.ts +++ b/src/plugins/data/public/search/search_strategy/index.ts @@ -17,13 +17,6 @@ * under the License. */ -export { - addSearchStrategy, - hasSearchStategyForIndexPattern, - getSearchStrategyById, - getSearchStrategyForSearchRequest, -} from './search_strategy_registry'; - export { SearchError, getSearchErrorType } from './search_error'; export { SearchStrategyProvider, SearchStrategySearchParams } from './types'; diff --git a/src/plugins/data/public/search/search_strategy/search_strategy_registry.test.ts b/src/plugins/data/public/search/search_strategy/search_strategy_registry.test.ts deleted file mode 100644 index eaf86e1b270d5..0000000000000 --- a/src/plugins/data/public/search/search_strategy/search_strategy_registry.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IndexPattern } from '../..'; -import { noOpSearchStrategy } from './no_op_search_strategy'; -import { - searchStrategies, - addSearchStrategy, - getSearchStrategyByViability, - getSearchStrategyById, - getSearchStrategyForSearchRequest, - hasSearchStategyForIndexPattern, -} from './search_strategy_registry'; -import { SearchStrategyProvider } from './types'; - -const mockSearchStrategies: SearchStrategyProvider[] = [ - { - id: '0', - isViable: (index: IndexPattern) => index.id === '0', - search: () => ({ - searching: Promise.resolve([]), - abort: () => void 0, - }), - }, - { - id: '1', - isViable: (index: IndexPattern) => index.id === '1', - search: () => ({ - searching: Promise.resolve([]), - abort: () => void 0, - }), - }, -]; - -describe('Search strategy registry', () => { - beforeEach(() => { - searchStrategies.length = 0; - }); - - describe('addSearchStrategy', () => { - it('adds a search strategy', () => { - addSearchStrategy(mockSearchStrategies[0]); - expect(searchStrategies.length).toBe(1); - }); - - it('does not add a search strategy if it is already included', () => { - addSearchStrategy(mockSearchStrategies[0]); - addSearchStrategy(mockSearchStrategies[0]); - expect(searchStrategies.length).toBe(1); - }); - }); - - describe('getSearchStrategyByViability', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns the viable strategy', () => { - expect(getSearchStrategyByViability({ id: '0' } as IndexPattern)).toBe( - mockSearchStrategies[0] - ); - expect(getSearchStrategyByViability({ id: '1' } as IndexPattern)).toBe( - mockSearchStrategies[1] - ); - }); - - it('returns undefined if there is no viable strategy', () => { - expect(getSearchStrategyByViability({ id: '-1' } as IndexPattern)).toBe(undefined); - }); - }); - - describe('getSearchStrategyById', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns the strategy by ID', () => { - expect(getSearchStrategyById('0')).toBe(mockSearchStrategies[0]); - expect(getSearchStrategyById('1')).toBe(mockSearchStrategies[1]); - }); - - it('returns undefined if there is no strategy with that ID', () => { - expect(getSearchStrategyById('-1')).toBe(undefined); - }); - - it('returns the noOp search strategy if passed that ID', () => { - expect(getSearchStrategyById('noOp')).toBe(noOpSearchStrategy); - }); - }); - - describe('getSearchStrategyForSearchRequest', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns the strategy by ID if provided', () => { - expect(getSearchStrategyForSearchRequest({}, { searchStrategyId: '1' })).toBe( - mockSearchStrategies[1] - ); - }); - - it('throws if there is no strategy by provided ID', () => { - expect(() => - getSearchStrategyForSearchRequest({}, { searchStrategyId: '-1' }) - ).toThrowErrorMatchingInlineSnapshot(`"No strategy with ID -1"`); - }); - - it('returns the strategy by viability if there is one', () => { - expect( - getSearchStrategyForSearchRequest({ - index: { - id: '1', - }, - }) - ).toBe(mockSearchStrategies[1]); - }); - - it('returns the no op strategy if there is no viable strategy', () => { - expect(getSearchStrategyForSearchRequest({ index: '3' })).toBe(noOpSearchStrategy); - }); - }); - - describe('hasSearchStategyForIndexPattern', () => { - beforeEach(() => { - mockSearchStrategies.forEach(addSearchStrategy); - }); - - it('returns whether there is a search strategy for this index pattern', () => { - expect(hasSearchStategyForIndexPattern({ id: '0' } as IndexPattern)).toBe(true); - expect(hasSearchStategyForIndexPattern({ id: '-1' } as IndexPattern)).toBe(false); - }); - }); -}); diff --git a/src/plugins/data/public/search/search_strategy/search_strategy_registry.ts b/src/plugins/data/public/search/search_strategy/search_strategy_registry.ts deleted file mode 100644 index 1ab6f7d4e1eff..0000000000000 --- a/src/plugins/data/public/search/search_strategy/search_strategy_registry.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { IndexPattern } from '../..'; -import { SearchStrategyProvider } from './types'; -import { noOpSearchStrategy } from './no_op_search_strategy'; -import { SearchResponse } from '..'; - -export const searchStrategies: SearchStrategyProvider[] = []; - -export const addSearchStrategy = (searchStrategy: SearchStrategyProvider) => { - if (searchStrategies.includes(searchStrategy)) { - return; - } - - searchStrategies.push(searchStrategy); -}; - -export const getSearchStrategyByViability = (indexPattern: IndexPattern) => { - return searchStrategies.find(searchStrategy => { - return searchStrategy.isViable(indexPattern); - }); -}; - -export const getSearchStrategyById = (searchStrategyId: string) => { - return [...searchStrategies, noOpSearchStrategy].find(searchStrategy => { - return searchStrategy.id === searchStrategyId; - }); -}; - -export const getSearchStrategyForSearchRequest = ( - searchRequest: SearchResponse, - { searchStrategyId }: { searchStrategyId?: string } = {} -) => { - // Allow the searchSource to declare the correct strategy with which to execute its searches. - if (searchStrategyId != null) { - const strategy = getSearchStrategyById(searchStrategyId); - if (!strategy) throw Error(`No strategy with ID ${searchStrategyId}`); - return strategy; - } - - // Otherwise try to match it to a strategy. - const viableSearchStrategy = getSearchStrategyByViability(searchRequest.index); - - if (viableSearchStrategy) { - return viableSearchStrategy; - } - - // This search strategy automatically rejects with an error. - return noOpSearchStrategy; -}; - -export const hasSearchStategyForIndexPattern = (indexPattern: IndexPattern) => { - return Boolean(getSearchStrategyByViability(indexPattern)); -}; diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx index 7183f14bdb255..36dcd4a00c05e 100644 --- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx +++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx @@ -35,7 +35,8 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { sortBy, isEqual } from 'lodash'; -import { SavedQuery, SavedQueryAttributes, SavedQueryService } from '../..'; +import { SavedQuery, SavedQueryService } from '../..'; +import { SavedQueryAttributes } from '../../query'; interface Props { savedQuery?: SavedQueryAttributes; diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index 26055a3ae41f7..b4ee02eefaf84 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -31,6 +31,13 @@ export const esSearchStrategyProvider: TSearchStrategyProvider { const config = await context.config$.pipe(first()).toPromise(); const defaultParams = getDefaultSearchParams(config); + + // Only default index pattern type is supported here. + // See data_enhanced for other type support. + if (!!request.indexType) { + throw new Error(`Unsupported index pattern type ${request.indexType}`); + } + const params = { ...defaultParams, ...request.params, diff --git a/src/plugins/data/server/search/routes.ts b/src/plugins/data/server/search/routes.ts index 2b8c4b95ee022..e618f99084aed 100644 --- a/src/plugins/data/server/search/routes.ts +++ b/src/plugins/data/server/search/routes.ts @@ -43,11 +43,11 @@ export function registerSearchRoute(router: IRouter): void { return res.ok({ body: response }); } catch (err) { return res.customError({ - statusCode: err.statusCode, + statusCode: err.statusCode || 500, body: { message: err.message, attributes: { - error: err.body.error, + error: err.body?.error || err.message, }, }, }); diff --git a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts index 2a1ffe79d033c..6982d3d2dba97 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/dependency_cache.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimefilterSetup } from 'src/plugins/data/public'; +import { DataPublicPluginSetup } from 'src/plugins/data/public'; import { IUiSettingsClient, ChromeStart, @@ -23,7 +23,7 @@ import { import { SecurityPluginSetup } from '../../../../../../plugins/security/public'; export interface DependencyCache { - timefilter: TimefilterSetup | null; + timefilter: DataPublicPluginSetup['query']['timefilter'] | null; config: IUiSettingsClient | null; indexPatterns: IndexPatternsContract | null; chrome: ChromeStart | null; diff --git a/x-pack/legacy/plugins/rollup/public/legacy.ts b/x-pack/legacy/plugins/rollup/public/legacy.ts index e3e663ac7b0f4..e137799bd34fe 100644 --- a/x-pack/legacy/plugins/rollup/public/legacy.ts +++ b/x-pack/legacy/plugins/rollup/public/legacy.ts @@ -7,7 +7,6 @@ import { npSetup, npStart } from 'ui/new_platform'; import { aggTypeFilters } from 'ui/agg_types'; import { aggTypeFieldFilters } from 'ui/agg_types'; -import { addSearchStrategy } from '../../../../../src/plugins/data/public'; import { RollupPlugin } from './plugin'; import { setup as management } from '../../../../../src/legacy/core_plugins/management/public/legacy'; @@ -18,7 +17,6 @@ export const setup = plugin.setup(npSetup.core, { __LEGACY: { aggTypeFilters, aggTypeFieldFilters, - addSearchStrategy, managementLegacy: management, }, }); diff --git a/x-pack/legacy/plugins/rollup/public/plugin.ts b/x-pack/legacy/plugins/rollup/public/plugin.ts index a01383f4733ef..2d2ff4c8449d8 100644 --- a/x-pack/legacy/plugins/rollup/public/plugin.ts +++ b/x-pack/legacy/plugins/rollup/public/plugin.ts @@ -7,14 +7,12 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { AggTypeFilters, AggTypeFieldFilters } from './legacy_imports'; -import { SearchStrategyProvider } from '../../../../../src/plugins/data/public'; import { ManagementSetup as ManagementSetupLegacy } from '../../../../../src/legacy/core_plugins/management/public/np_ready'; import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_management'; // @ts-ignore import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config'; // @ts-ignore import { RollupIndexPatternListConfig } from './index_pattern_list/rollup_index_pattern_list_config'; -import { getRollupSearchStrategy } from './search/rollup_search_strategy'; // @ts-ignore import { initAggTypeFilter } from './visualize/agg_type_filter'; // @ts-ignore @@ -37,7 +35,6 @@ export interface RollupPluginSetupDependencies { __LEGACY: { aggTypeFilters: AggTypeFilters; aggTypeFieldFilters: AggTypeFieldFilters; - addSearchStrategy: (searchStrategy: SearchStrategyProvider) => void; managementLegacy: ManagementSetupLegacy; }; home?: HomePublicPluginSetup; @@ -49,7 +46,7 @@ export class RollupPlugin implements Plugin { setup( core: CoreSetup, { - __LEGACY: { aggTypeFilters, aggTypeFieldFilters, addSearchStrategy, managementLegacy }, + __LEGACY: { aggTypeFilters, aggTypeFieldFilters, managementLegacy }, home, management, indexManagement, @@ -67,7 +64,6 @@ export class RollupPlugin implements Plugin { if (isRollupIndexPatternsEnabled) { managementLegacy.indexPattern.creation.add(RollupIndexPatternCreationConfig); managementLegacy.indexPattern.list.add(RollupIndexPatternListConfig); - addSearchStrategy(getRollupSearchStrategy(core.http.fetch)); initAggTypeFilter(aggTypeFilters); initAggTypeFieldFilter(aggTypeFieldFilters); } diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 6e12ffb6404c6..69b357196dc32 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -30,7 +30,7 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b64caea05dbe4..5b55ad3433e22 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1116,7 +1116,6 @@ "kbn.discover.noResults.indexFailureIndexText": "インデックス {failureIndex}", "kbn.discover.noResults.indexFailureShardText": "{index}、シャード {failureShard}", "kbn.discover.noResults.queryMayNotMatchTitle": "表示されているインデックスの 1 つまたは複数にデータフィールドが含まれています。クエリが現在の時間範囲のデータと一致しないか、現在選択された時間範囲にデータが全く存在しない可能性があります。データが存在する時間範囲に変えることができます。", - "kbn.discover.noResults.requiredPluginIsNotInstalledOrDisabledTitle": "{unsupportedType} インデックスに基づくインデックスパターンには X-Pack の {unsupportedType} プラグインが必要で、このプラグインは現在インストールされていないか無効になっています。", "kbn.discover.noResults.searchExamples.400to499StatusCodeExampleTitle": "400-499 のすべてのステータスコードを検索", "kbn.discover.noResults.searchExamples.400to499StatusCodeWithPhpExtensionExampleTitle": "400-499 の php 拡張子のステータスコードを検索", "kbn.discover.noResults.searchExamples.400to499StatusCodeWithPhpOrHtmlExtensionExampleTitle": "400-499 の php または html 拡張子のステータスコードを検索", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 872ec3b71ec10..2bf6d0db04a80 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1116,7 +1116,6 @@ "kbn.discover.noResults.indexFailureIndexText": "索引 {failureIndex}", "kbn.discover.noResults.indexFailureShardText": "{index},分片 {failureShard}", "kbn.discover.noResults.queryMayNotMatchTitle": "您正在查看的一个或多个索引包含日期字段。您的查询在当前时间范围内可能不匹配任何数据,也可能在当前选定的时间范围内没有任何数据。您可以尝试将时间范围更改为包含数据的时间范围。", - "kbn.discover.noResults.requiredPluginIsNotInstalledOrDisabledTitle": "基于 {unsupportedType} 索引的索引模式需要 X-Pack 的 {unsupportedType} 插件,但其未安装或已禁用", "kbn.discover.noResults.searchExamples.400to499StatusCodeExampleTitle": "查找所有介于 400-499 之间的状态代码", "kbn.discover.noResults.searchExamples.400to499StatusCodeWithPhpExtensionExampleTitle": "查找状态代码 400-499 以及扩展名 php", "kbn.discover.noResults.searchExamples.400to499StatusCodeWithPhpOrHtmlExtensionExampleTitle": "查找状态代码 400-499 以及扩展名 php 或 html",